src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore10
-rw-r--r--.prettierignore3
-rw-r--r--.prettierrc.json8
-rw-r--r--LICENSE21
-rw-r--r--README.md287
-rw-r--r--config.example.json5
-rw-r--r--data/.gitkeep0
-rw-r--r--mattertimesync.service15
-rw-r--r--mattertimesync.timer10
-rw-r--r--package-lock.json2721
-rw-r--r--package.json39
-rw-r--r--src/cli.ts306
-rw-r--r--src/commissioning.ts101
-rw-r--r--src/config.ts99
-rw-r--r--src/controller.ts96
-rw-r--r--src/device.ts72
-rw-r--r--src/fabrics.ts153
-rw-r--r--src/inspection.ts272
-rw-r--r--src/logging.ts71
-rw-r--r--src/state.ts90
-rw-r--r--src/sync.ts120
-rw-r--r--src/timezone.ts106
-rw-r--r--test/commissioning.test.ts33
-rw-r--r--test/config.test.ts73
-rw-r--r--test/fabrics.test.ts46
-rw-r--r--test/state.test.ts103
-rw-r--r--test/sync.test.ts111
-rw-r--r--test/timezone.test.ts66
-rw-r--r--tsconfig.json21
29 files changed, 5058 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..37c2350
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+node_modules/
+dist/
+
+# Persistent Matter fabric state and credentials must never be committed.
+data/*
+!data/.gitkeep
+
+*.log
+*.tgz
+mattertimesync.mjs
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..3e0aed8
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,3 @@
+package-lock.json
+dist/
+data/
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..e242695
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,8 @@
+{
+ "printWidth": 110,
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "arrowParens": "avoid"
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d43b2af
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Luke Hoersten
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c00969a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,287 @@
+# mattertimesync
+
+A small CLI Matter controller that sets the clocks of Matter devices via the standard Time
+Synchronization cluster.
+
+Some Matter devices with clock displays (air-quality monitors, thermostats, sensor hubs) lose their
+clock after a power outage, and not every ecosystem restores it reliably. This tool joins each device's
+existing Matter setup as an _additional_ controller (Matter multi-admin) and pushes the correct time to
+every device commissioned onto its fabric. Run it periodically from a systemd timer; the primary
+ecosystem (e.g. Apple Home) keeps working unchanged.
+
+There is no daemon: each invocation connects, syncs, and exits.
+
+The main repository is <https://src.nth.io/luke/mattertimesync/>; the
+[GitHub repository](https://github.com/lukehoersten/mattertimesync) is a backup mirror.
+
+## How it works
+
+- The device stays commissioned to its primary ecosystem and attached to its existing Thread or Wi-Fi
+ network.
+- A Raspberry Pi (or any Linux host) runs this tool as a second Matter administrator on its own fabric.
+- For Thread devices, the host needs no Thread radio, no OpenThread Border Router, no Thread
+ credentials, and no BLE. It reaches the device over IPv6 through the existing Thread Border Routers
+ (e.g. HomePod, Apple TV).
+- The host's own clock is NTP-synchronized; the tool refuses to push time from an unsynchronized clock.
+
+Requirements on the host:
+
+- Node.js 20 or newer (current LTS recommended)
+- working IPv6 and multicast DNS on the network shared with the device's border router
+- system time synchronized via NTP (`systemd-timesyncd` or equivalent)
+
+## Design
+
+Principles the implementation holds to:
+
+- **One-shot CLI, not a daemon.** Being a secondary Matter controller is fabric membership, not a running
+ process. During commissioning the device permanently stores this controller's fabric entry (root
+ certificate and operational credential) in its own flash, alongside the primary ecosystem's; that
+ relationship persists with no process running. Each invocation loads the stored keys, discovers the
+ device, opens a fresh CASE session, does its work, and exits. A daemon's only real advantage would be
+ reactivity: a held subscription notices a device reboot within seconds. The one-shot design bounds clock
+ staleness to the timer interval instead, and in exchange drops all long-lived-connection machinery
+ (reconnect detection, backoff, debouncing, in-process scheduling). For a clock display after a rare power
+ outage, "wrong for at most an hour" is an acceptable trade, and the interval can be tightened since each
+ run costs only seconds. If second-level reactivity is ever wanted, a `watch` command holding a
+ subscription is an additive feature on the same modules, not a redesign.
+- **Capability-driven, never vendor-specific.** Any Matter device exposing the standard Time
+ Synchronization cluster (0x38) works. Sync behavior is driven by a live inspection of the device's
+ actual features, never by assumptions about a particular product; optional features a device lacks are
+ skipped with the limitation logged (UTC-only devices get UTC only), and a missing cluster is a clear
+ compatibility error.
+- **The fabric is created exactly once.** All Matter identity and fabric data persists in `storagePath`
+ and is never recreated on launch. Reconnection uses Matter operational discovery from the saved node
+ identity, never a stored IP address, so border-router changes, device reboots, and mDNS churn are
+ tolerated.
+- **Fail safely.** The device is never recommissioned automatically, and time is never pushed from a host
+ clock that is not NTP-synchronized. Transient failures exit non-zero and the next timer tick retries.
+- **Scheduling belongs to systemd**, not the application: the timer unit owns the interval, and device
+ membership lives in controller storage, so the configuration stays three fields.
+- **Matter time is bigint end to end.** Matter UTC time is microseconds since 2000-01-01T00:00:00Z (not
+ the Unix epoch); the full timestamp never passes through a JavaScript `number`. Time zones are IANA
+ names with offsets and DST transitions derived from the host's time-zone database, never fixed offsets.
+- **Intentionally small.** No GUI, database, cloud service, Home Assistant integration, MQTT, REST API,
+ metrics, Thread Border Router functionality, BLE commissioning, or firmware updates. Multiple devices
+ are supported, but only as the same sync applied to each commissioned node.
+
+## Project status
+
+Implemented: project scaffold, configuration, persistent controller fabric, on-network multi-admin
+commissioning, and full endpoint/cluster inspection.
+
+The Time Synchronization write commands (the `sync` command) are intentionally not implemented yet:
+they will be written against a real device's inspected capabilities, not against assumptions. The next
+milestone is a successful commissioning plus a complete cluster dump (`inspect --json`) from real
+hardware.
+
+## Build
+
+```bash
+git clone <repository>
+cd mattertimesync
+npm install
+npm run build
+```
+
+Run tests and lint:
+
+```bash
+npm test
+npm run lint
+```
+
+## Packaging for deployment
+
+All runtime dependencies are pure JavaScript (no native modules), so the whole tool bundles into one
+self-contained file. The target machine needs only `node` (20 or newer): no npm, no registry access, no
+git checkout, no build.
+
+```bash
+npm run bundle # produces mattertimesync.mjs (~4.5 MB)
+scp mattertimesync.mjs pi:
+ssh pi sudo install -D -m 0755 mattertimesync.mjs /opt/mattertimesync/mattertimesync.mjs
+```
+
+Run it as `node /opt/mattertimesync/mattertimesync.mjs ...` (the systemd unit does exactly this).
+Upgrades replace the one file; Matter state under `/var/lib/mattertimesync` is untouched.
+
+Alternative, if you prefer a `mattertimesync` command on PATH and don't mind npm on the target:
+`npm pack` produces a small tarball whose `npm install -g <tarball>` pulls dependencies from the
+registry (adjust the unit's `ExecStart` accordingly).
+
+## Configuration
+
+The CLI reads `/etc/mattertimesync/config.json` by default; override with `--config <path>`.
+See `config.example.json`:
+
+```json
+{
+ "storagePath": "/var/lib/mattertimesync",
+ "timezone": "America/Chicago",
+ "logLevel": "info"
+}
+```
+
+| Field | Default | Meaning |
+| ------------- | ----------------- | --------------------------------------------------------------- |
+| `storagePath` | (required) | Directory for persistent Matter fabric state. Contains secrets. |
+| `timezone` | `America/Chicago` | IANA time-zone name. Never a fixed UTC offset; DST is derived. |
+| `logLevel` | `info` | `debug`, `info`, `warn`, or `error`. |
+
+Scheduling lives in the systemd timer, and device membership lives in controller storage; neither is in
+the configuration. The set of devices kept in sync is exactly the set commissioned onto this
+controller's fabric, changed only by `commission` and `decommission`.
+
+The storage directory holds the controller's private keys and operational certificates. Keep it mode
+`0700`, owned by the service user, and out of Git. Deleting it destroys this controller's fabric
+identity; the stale fabric would then need to be removed from the device before commissioning again.
+
+## Commissioning
+
+1. In the device's primary ecosystem, open its pairing mode (Apple Home: device settings, _Turn On
+ Pairing Mode_). The ecosystem shows a temporary setup code.
+2. Within the pairing window, run (as the service user, so the timer job can reuse the same storage):
+
+```bash
+sudo -u mattertimesync \
+ node /opt/mattertimesync/mattertimesync.mjs \
+ --config /etc/mattertimesync/config.json \
+ commission --code 12345678901
+```
+
+Omit `--code` to be prompted interactively. The code is used once, never logged, and never stored.
+
+Commissioning discovers the device via `_matterc._udp` on the IP network, joins it to this controller's
+fabric, prints a full endpoint and cluster inspection, and records the node ID. The controller fabric is
+created once and reused for every device and every subsequent start.
+
+To sync several devices, repeat the process per device: Matter multi-admin pairing is per-device, so
+each device's pairing mode is opened individually in the primary ecosystem, yielding one code per
+device. A device already on this fabric rejects re-commissioning by itself (fabric conflict), so
+duplicates cannot occur.
+
+## CLI
+
+```bash
+mattertimesync commission [--code <pairing-code>]
+mattertimesync nodes
+mattertimesync inspect [--node <id>] [--json]
+mattertimesync sync [--node <id>] # not yet implemented (pending real-device inspection)
+mattertimesync status
+mattertimesync fabrics [--node <id>] [--remove <index>]
+mattertimesync decommission --node <id>
+```
+
+`--node <id>` selects which device a command targets; it may be omitted while only one device is
+commissioned. `sync` targets all commissioned devices by default.
+
+- `nodes` lists every commissioned device with its cached vendor/product name and per-device last
+ connection and sync results.
+- `inspect` connects to the selected node and prints vendor/product information, every endpoint with its
+ server and client clusters, and the Time Synchronization cluster's feature map, supported commands,
+ and attribute values. `--json` emits the same data machine-readable (node IDs and timestamps as
+ strings, safe for 64-bit values).
+- `sync` will connect to each targeted device, verify the host clock is NTP-synchronized, set UTC time,
+ and configure the time zone and DST transitions when the device supports them, then exit. For each
+ device it reports the correction made: the device's clock before the sync (or "unset" after a power
+ loss), the time written, and the delta, e.g. `device clock was 1m 23s behind`.
+- `status` prints controller state, configured time zone with the current UTC offset and next DST
+ transition, host NTP status, and per-device last connection/sync results.
+- `fabrics` reads the selected device's Operational Credentials fabric table and prints every
+ commissioned controller (label, vendor, fabric and node IDs, slot usage), marks this controller's own
+ entry, and flags likely stale entries: ones carrying our label but belonging to an old identity whose
+ storage was deleted. `--remove <index>` deletes a stale entry from the device (our own entry is
+ refused; use `decommission` for that).
+- `decommission` gracefully removes this controller's fabric from the selected device (the primary
+ ecosystem is untouched) and drops its state entry. Delete `storagePath` only after every device has
+ been decommissioned.
+
+## Install with a systemd timer
+
+Install the bundled file first (see "Packaging for deployment"), then create the service account and
+directories:
+
+```bash
+sudo useradd \
+ --system \
+ --home /var/lib/mattertimesync \
+ --shell /usr/sbin/nologin \
+ mattertimesync
+
+sudo mkdir -p /etc/mattertimesync
+sudo mkdir -p /var/lib/mattertimesync
+
+sudo chown -R mattertimesync:mattertimesync /var/lib/mattertimesync
+sudo chmod 700 /var/lib/mattertimesync
+```
+
+Place the configuration at `/etc/mattertimesync/config.json` and install the units (the service runs
+the bundled file at `/opt/mattertimesync/mattertimesync.mjs`):
+
+```bash
+sudo cp mattertimesync.service mattertimesync.timer /etc/systemd/system/
+sudo systemctl daemon-reload
+```
+
+Commission first (see above), then enable the timer:
+
+```bash
+sudo systemctl enable --now mattertimesync.timer
+```
+
+The timer runs a sync 2 minutes after boot and hourly thereafter (see `mattertimesync.timer` to adjust).
+Run a sync on demand with `sudo systemctl start mattertimesync.service`.
+
+## Backup and restore
+
+Back up:
+
+```text
+/var/lib/mattertimesync
+/etc/mattertimesync/config.json
+```
+
+Restoring both paths preserves the controller fabric credentials, the device's node ID, the controller
+identity, and service state, so no recommissioning is needed. Keep backups as restricted as the
+originals; the storage directory contains private keys.
+
+## Removing this controller from the device
+
+Run `mattertimesync decommission` to undo commissioning: the device drops this controller's fabric while
+staying paired to its primary ecosystem, and deleting `/var/lib/mattertimesync` becomes safe.
+
+Order matters. Deleting the storage directory first destroys the keys, leaving a stale fabric entry
+occupying one of the device's limited fabric slots (spec minimum is 5). Orphans accumulate if storage is
+deleted and the device recommissioned repeatedly (e.g. during testing). To find and clean them, run
+`mattertimesync fabrics`: stale entries carry our label but not our current identity, and
+`fabrics --remove <index>` deletes them using the live controller's admin rights. Alternatively, remove
+them in Apple Home (device settings, _Connected Services_) or, as a last resort, factory-reset the
+device and re-pair it.
+
+## Troubleshooting
+
+```bash
+ip -6 addr
+ip -6 route
+avahi-browse -art
+timedatectl status
+timedatectl show -p NTPSynchronized
+journalctl -u mattertimesync.service -f
+systemctl list-timers mattertimesync.timer
+```
+
+**Commissionable device not found**: confirm the pairing window is still open (it expires), that the
+host and the border router are on networks that permit mDNS/multicast, that IPv6 is enabled, and that
+no firewall or container bridge blocks Matter UDP traffic.
+
+**Commissioning succeeds but operational connection fails**: check IPv6 routing to the device's
+network, `_matter._tcp` DNS-SD discovery, that the persisted storage path is readable by the user
+running the CLI, and that the border routers are reachable.
+
+**Clock is off by one hour**: a fixed offset was used instead of IANA rules, or DST configuration is
+missing or rejected. This tool always derives offsets from the IANA database.
+
+**Clock shows an absurd future date**: the Unix epoch was used instead of the Matter epoch
+(2000-01-01T00:00:00Z), or seconds were sent where microseconds were expected. This tool converts with
+bigint arithmetic against the Matter epoch only.
diff --git a/config.example.json b/config.example.json
new file mode 100644
index 0000000..2700ecb
--- /dev/null
+++ b/config.example.json
@@ -0,0 +1,5 @@
+{
+ "storagePath": "/var/lib/mattertimesync",
+ "timezone": "America/Chicago",
+ "logLevel": "info"
+}
diff --git a/data/.gitkeep b/data/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/data/.gitkeep
diff --git a/mattertimesync.service b/mattertimesync.service
new file mode 100644
index 0000000..fa69907
--- /dev/null
+++ b/mattertimesync.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=Matter device clock synchronization
+Wants=network-online.target
+After=network-online.target time-sync.target
+
+[Service]
+Type=oneshot
+User=mattertimesync
+Group=mattertimesync
+ExecStart=/usr/bin/node /opt/mattertimesync/mattertimesync.mjs --config /etc/mattertimesync/config.json sync
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=/var/lib/mattertimesync
diff --git a/mattertimesync.timer b/mattertimesync.timer
new file mode 100644
index 0000000..4d7b64a
--- /dev/null
+++ b/mattertimesync.timer
@@ -0,0 +1,10 @@
+[Unit]
+Description=Periodic Matter device clock synchronization
+
+[Timer]
+OnBootSec=2min
+OnUnitActiveSec=1h
+RandomizedDelaySec=5min
+
+[Install]
+WantedBy=timers.target
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..f638612
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2721 @@
+{
+ "name": "mattertimesync",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mattertimesync",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@matter/main": "^0.17.6",
+ "@project-chip/matter.js": "^0.17.6"
+ },
+ "bin": {
+ "mattertimesync": "dist/cli.js"
+ },
+ "devDependencies": {
+ "@types/node": "^26.1.1",
+ "esbuild": "^0.28.1",
+ "oxlint": "^1.0.0",
+ "prettier": "^3.6.0",
+ "typescript": "^7.0.2",
+ "vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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/@matter/general": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.17.6.tgz",
+ "integrity": "sha512-PiSly47lwR+31sMWZTxT9eVgibfC2PB3FflTwxhVDzektrNNYT4wh9+5abTNhQmszs/VnXixbyo64HSz+LQS8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/curves": "^2.2.0"
+ }
+ },
+ "node_modules/@matter/main": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.17.6.tgz",
+ "integrity": "sha512-RBXVLxJGEd3IUZ63QrHPUpb7o1amErvs2MoK3bJpnUXYNqXAuN2IHLTYZ04j4yrdMHJlyGSDxdIYr5rN6zXqSA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@matter/general": "0.17.6",
+ "@matter/model": "0.17.6",
+ "@matter/node": "0.17.6",
+ "@matter/protocol": "0.17.6",
+ "@matter/types": "0.17.6"
+ },
+ "optionalDependencies": {
+ "@matter/nodejs": "0.17.6"
+ }
+ },
+ "node_modules/@matter/model": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.17.6.tgz",
+ "integrity": "sha512-ECf6r0zTq6FS6IA4ov8qtSu36iyO1oxEJT06RFYX/SVDOyfhgzRjQDSIsVtW4UwTMht8kSVnDJE9EAO0qY37YA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@matter/general": "0.17.6"
+ }
+ },
+ "node_modules/@matter/node": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.17.6.tgz",
+ "integrity": "sha512-qtkh3/hz+73Wh7kY+Jz9HogrnpqjIMreYaY//VEMb9Jl/tY1A+3RFa7PDIY51LK2m+gWy3boXu/3kdYEvn00ig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@matter/general": "0.17.6",
+ "@matter/model": "0.17.6",
+ "@matter/protocol": "0.17.6",
+ "@matter/types": "0.17.6"
+ }
+ },
+ "node_modules/@matter/nodejs": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.17.6.tgz",
+ "integrity": "sha512-tvn2wLMgjd1YVKr/kAkKq50WU+8mUeoDVCPIyO6aXJMsy0GTOc8/vC3Lu3ZIUD1IwMiOgn67xnqBVozpp+ZjTw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@matter/general": "0.17.6",
+ "@matter/node": "0.17.6",
+ "@matter/protocol": "0.17.6",
+ "@matter/types": "0.17.6"
+ },
+ "engines": {
+ "node": ">=20.19.0 <22.0.0 || >=22.13.0"
+ }
+ },
+ "node_modules/@matter/protocol": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.17.6.tgz",
+ "integrity": "sha512-p3oc8v2RKKbV4RkkjKemDpzheNHFMQJnbxpij2yKpxsXYIXpYnwtexnvLxDpZCJPsd9Jv3igSxxURBEUexHmBA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@matter/general": "0.17.6",
+ "@matter/model": "0.17.6",
+ "@matter/types": "0.17.6"
+ }
+ },
+ "node_modules/@matter/types": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.17.6.tgz",
+ "integrity": "sha512-zYCWx+om6hQR6s6CHniWJMaOsqzTNDKcxUoU0QI8ZgF39SCtIeIb+qOzNigoAtKKZzhVdo9B5IsbEbYY4PhO3g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@matter/general": "0.17.6",
+ "@matter/model": "0.17.6"
+ }
+ },
+ "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/@noble/curves": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz",
+ "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "2.2.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
+ "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.75.0.tgz",
+ "integrity": "sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.75.0.tgz",
+ "integrity": "sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.75.0.tgz",
+ "integrity": "sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.75.0.tgz",
+ "integrity": "sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.75.0.tgz",
+ "integrity": "sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.75.0.tgz",
+ "integrity": "sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.75.0.tgz",
+ "integrity": "sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.75.0.tgz",
+ "integrity": "sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.75.0.tgz",
+ "integrity": "sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.75.0.tgz",
+ "integrity": "sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.75.0.tgz",
+ "integrity": "sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.75.0.tgz",
+ "integrity": "sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.75.0.tgz",
+ "integrity": "sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.75.0.tgz",
+ "integrity": "sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.75.0.tgz",
+ "integrity": "sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.75.0.tgz",
+ "integrity": "sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.75.0.tgz",
+ "integrity": "sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.75.0.tgz",
+ "integrity": "sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.75.0.tgz",
+ "integrity": "sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@project-chip/matter.js": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.17.6.tgz",
+ "integrity": "sha512-E/FvTvY3G8cPTbpMJDgYdUY8oUHkWN47dXJ6aRjmiDrLFToRb40rrW53p0g6wOOvMnWE9nhVLbJxbE9kOpJIyg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@matter/general": "0.17.6",
+ "@matter/model": "0.17.6",
+ "@matter/node": "0.17.6",
+ "@matter/protocol": "0.17.6",
+ "@matter/types": "0.17.6"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@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/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/@typescript/typescript-aix-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
+ "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
+ "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
+ "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
+ "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
+ "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
+ "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-loong64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
+ "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-mips64el": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
+ "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
+ "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-riscv64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
+ "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-s390x": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
+ "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
+ "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-sunos-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
+ "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
+ "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
+ "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
+ "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
+ "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.10",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
+ "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
+ "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.10",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
+ "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
+ "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
+ "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/es-module-lexer": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
+ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/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/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+ "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/oxlint": {
+ "version": "1.75.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.75.0.tgz",
+ "integrity": "sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "oxlint": "bin/oxlint"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.75.0",
+ "@oxlint/binding-android-arm64": "1.75.0",
+ "@oxlint/binding-darwin-arm64": "1.75.0",
+ "@oxlint/binding-darwin-x64": "1.75.0",
+ "@oxlint/binding-freebsd-x64": "1.75.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.75.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.75.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.75.0",
+ "@oxlint/binding-linux-arm64-musl": "1.75.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.75.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.75.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.75.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.75.0",
+ "@oxlint/binding-linux-x64-gnu": "1.75.0",
+ "@oxlint/binding-linux-x64-musl": "1.75.0",
+ "@oxlint/binding-openharmony-arm64": "1.75.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.75.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.75.0",
+ "@oxlint/binding-win32-x64-msvc": "1.75.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=7.0.2001",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+ "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc"
+ },
+ "engines": {
+ "node": ">=16.20.0"
+ },
+ "optionalDependencies": {
+ "@typescript/typescript-aix-ppc64": "7.0.2",
+ "@typescript/typescript-darwin-arm64": "7.0.2",
+ "@typescript/typescript-darwin-x64": "7.0.2",
+ "@typescript/typescript-freebsd-arm64": "7.0.2",
+ "@typescript/typescript-freebsd-x64": "7.0.2",
+ "@typescript/typescript-linux-arm": "7.0.2",
+ "@typescript/typescript-linux-arm64": "7.0.2",
+ "@typescript/typescript-linux-loong64": "7.0.2",
+ "@typescript/typescript-linux-mips64el": "7.0.2",
+ "@typescript/typescript-linux-ppc64": "7.0.2",
+ "@typescript/typescript-linux-riscv64": "7.0.2",
+ "@typescript/typescript-linux-s390x": "7.0.2",
+ "@typescript/typescript-linux-x64": "7.0.2",
+ "@typescript/typescript-netbsd-arm64": "7.0.2",
+ "@typescript/typescript-netbsd-x64": "7.0.2",
+ "@typescript/typescript-openbsd-arm64": "7.0.2",
+ "@typescript/typescript-openbsd-x64": "7.0.2",
+ "@typescript/typescript-sunos-x64": "7.0.2",
+ "@typescript/typescript-win32-arm64": "7.0.2",
+ "@typescript/typescript-win32-x64": "7.0.2"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
+ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.17",
+ "rolldown": "~1.1.5",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
+ "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.10",
+ "@vitest/mocker": "4.1.10",
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/runner": "4.1.10",
+ "@vitest/snapshot": "4.1.10",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.10",
+ "@vitest/browser-preview": "4.1.10",
+ "@vitest/browser-webdriverio": "4.1.10",
+ "@vitest/coverage-istanbul": "4.1.10",
+ "@vitest/coverage-v8": "4.1.10",
+ "@vitest/ui": "4.1.10",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a60f08e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "mattertimesync",
+ "version": "0.1.0",
+ "description": "CLI Matter controller that synchronizes the clocks of Matter devices via the standard Time Synchronization cluster",
+ "type": "module",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "bin": {
+ "mattertimesync": "dist/cli.js"
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "bundle": "npm run clean && npm run build && esbuild dist/cli.js --bundle --platform=node --target=node20 --format=esm --external:bun:sqlite --external:*/BunSqlite.js --banner:js=\"import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);\" --outfile=mattertimesync.mjs",
+ "prepack": "npm run clean && npm run build",
+ "clean": "rm -rf dist",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "lint": "oxlint src test",
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.json\" \"*.md\"",
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"*.json\" \"*.md\""
+ },
+ "dependencies": {
+ "@matter/main": "^0.17.6",
+ "@project-chip/matter.js": "^0.17.6"
+ },
+ "devDependencies": {
+ "@types/node": "^26.1.1",
+ "esbuild": "^0.28.1",
+ "oxlint": "^1.0.0",
+ "prettier": "^3.6.0",
+ "typescript": "^7.0.2",
+ "vitest": "^4.1.10"
+ }
+}
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 0000000..382c40b
--- /dev/null
+++ b/src/cli.ts
@@ -0,0 +1,306 @@
+#!/usr/bin/env node
+import { existsSync } from "node:fs";
+import { createInterface } from "node:readline";
+import { parseArgs } from "node:util";
+import { ConfigError, DEFAULT_CONFIG_PATH, loadConfig, parseNodeId, type Config } from "./config.js";
+import { commissionedNodeIds, resolveSingleNode, startController } from "./controller.js";
+import { commissionDevice } from "./commissioning.js";
+import { connectDevice } from "./device.js";
+import { formatFabricTable, readFabricTable, removeFabricByIndex } from "./fabrics.js";
+import { formatInspection, inspectionToJson, inspectNode } from "./inspection.js";
+import { Log, describeError, setLogLevel } from "./logging.js";
+import { currentMatterUtcMicroseconds, isHostClockSynchronized } from "./sync.js";
+import { nodeState, readServiceState, removeNodeState, serviceStatePath, updateNodeState } from "./state.js";
+import { formatUtcOffset, nextOffsetTransition, utcOffsetSeconds } from "./timezone.js";
+
+const log = new Log("cli");
+
+const USAGE = `Usage: mattertimesync [--config <path>] <command> [options]
+
+Commands:
+ commission [--code <pairing-code>] Commission a device as an additional Matter admin
+ nodes List commissioned devices and their sync state
+ inspect [--node <id>] [--json] Dump a device's endpoints, clusters, time capabilities
+ sync [--node <id>] Synchronize device clocks (not yet implemented)
+ status Show controller and per-device state
+ fabrics [--node <id>] [--remove <n>] List a device's fabric table; remove a stale entry
+ decommission --node <id> Remove this controller from a device
+
+The controller fabric is the device registry: every commissioned node is kept
+in sync. --node may be omitted while only one device is commissioned.
+Run "sync" periodically (e.g. from a systemd timer) to keep clocks correct.
+
+Options:
+ --config <path> Configuration file (default: ${DEFAULT_CONFIG_PATH})
+ --help Show this help
+`;
+
+async function main(): Promise<number> {
+ const { values, positionals } = parseArgs({
+ options: {
+ config: { type: "string" },
+ code: { type: "string" },
+ json: { type: "boolean", default: false },
+ node: { type: "string" },
+ remove: { type: "string" },
+ help: { type: "boolean", default: false },
+ },
+ allowPositionals: true,
+ });
+
+ if (values.help || positionals.length === 0) {
+ process.stdout.write(USAGE);
+ return values.help ? 0 : 2;
+ }
+
+ const command = positionals[0]!;
+ const configPath = values.config ?? DEFAULT_CONFIG_PATH;
+
+ let config: Config;
+ let requestedNode: bigint | undefined;
+ try {
+ config = loadConfig(configPath);
+ requestedNode = values.node === undefined ? undefined : parseNodeId(values.node);
+ } catch (cause) {
+ if (cause instanceof ConfigError) {
+ process.stderr.write(`Configuration error: ${cause.message}\n`);
+ return 1;
+ }
+ throw cause;
+ }
+ setLogLevel(config.logLevel);
+
+ switch (command) {
+ case "commission":
+ return await runCommission(config, values.code);
+ case "nodes":
+ return await runNodes(config);
+ case "inspect":
+ return await runInspect(config, requestedNode, values.json ?? false);
+ case "sync":
+ return runNotImplemented("sync");
+ case "status":
+ return await runStatus(config);
+ case "fabrics":
+ return await runFabrics(config, requestedNode, values.remove);
+ case "decommission":
+ return await runDecommission(config, requestedNode);
+ default:
+ process.stderr.write(`Unknown command "${command}"\n\n${USAGE}`);
+ return 2;
+ }
+}
+
+async function runCommission(config: Config, codeArgument: string | undefined): Promise<number> {
+ const pairingCode = codeArgument ?? (await promptForPairingCode());
+ const controller = await startController(config);
+ try {
+ const result = await commissionDevice(controller, config, pairingCode);
+
+ const node = await connectDevice(controller, result.nodeId);
+ updateNodeState(serviceStatePath(config.storagePath), result.nodeId, {
+ lastSuccessfulConnection: new Date().toISOString(),
+ });
+ const report = await inspectNode(node);
+ process.stdout.write(formatInspection(report) + "\n\n");
+
+ process.stdout.write(
+ [
+ "Commissioning summary",
+ ` Node ID: ${result.nodeId}`,
+ ` Fabric ID: ${controller.fabric.fabricId}`,
+ ` Total devices: ${commissionedNodeIds(controller).length}`,
+ ` Time Sync: ${
+ report.timeSynchronization
+ ? `endpoint ${report.timeSynchronization.endpointId}`
+ : "cluster not found"
+ }`,
+ "",
+ "The device remains paired with its primary ecosystem; this controller",
+ "was added as an additional Matter administrator. Time synchronization",
+ "commands will be implemented from this inspection data (plan Phase 5).",
+ "",
+ ].join("\n"),
+ );
+ return 0;
+ } finally {
+ await controller.close();
+ }
+}
+
+async function runNodes(config: Config): Promise<number> {
+ const controller = await startController(config);
+ try {
+ const details = controller.getCommissionedNodesDetails();
+ if (details.length === 0) {
+ process.stdout.write('No devices commissioned. Run "commission" to add one.\n');
+ return 0;
+ }
+
+ const state = readServiceState(serviceStatePath(config.storagePath));
+ process.stdout.write(`${details.length} commissioned device${details.length === 1 ? "" : "s"}:\n`);
+ for (const detail of details) {
+ const nodeId = BigInt(detail.nodeId);
+ const info = detail.deviceData.basicInformation ?? {};
+ const name = [info["vendorName"], info["productName"]].filter(Boolean).join(" ");
+ const perNode = nodeState(state, nodeId);
+ process.stdout.write(
+ [
+ ` node ${nodeId}: ${name || "(no cached device info)"}`,
+ ` last connection: ${perNode.lastSuccessfulConnection ?? "never"}`,
+ ` last sync: ${perNode.lastSuccessfulSync ?? "never"}`,
+ ...(perNode.lastError !== null ? [` last error: ${perNode.lastError}`] : []),
+ ].join("\n") + "\n",
+ );
+ }
+ return 0;
+ } finally {
+ await controller.close();
+ }
+}
+
+async function runInspect(config: Config, requestedNode: bigint | undefined, json: boolean): Promise<number> {
+ const controller = await startController(config);
+ try {
+ const nodeId = resolveSingleNode(controller, requestedNode);
+ const node = await connectDevice(controller, nodeId);
+ updateNodeState(serviceStatePath(config.storagePath), nodeId, {
+ lastSuccessfulConnection: new Date().toISOString(),
+ });
+ const report = await inspectNode(node);
+ process.stdout.write((json ? inspectionToJson(report) : formatInspection(report)) + "\n");
+ return 0;
+ } finally {
+ await controller.close();
+ }
+}
+
+function runNotImplemented(command: string): number {
+ process.stderr.write(
+ `"${command}" is not implemented yet. Per the project plan, the Time Synchronization\n` +
+ `write commands are implemented only after inspecting the real device.\n` +
+ `Run "commission" and then "inspect --json" against the hardware first.\n`,
+ );
+ return 3;
+}
+
+async function runStatus(config: Config): Promise<number> {
+ const statePath = serviceStatePath(config.storagePath);
+ const state = readServiceState(statePath);
+ const storageExists = existsSync(config.storagePath);
+ const ntpSynchronized = await isHostClockSynchronized();
+ const offsetSeconds = utcOffsetSeconds(config.timezone);
+ const transition = nextOffsetTransition(config.timezone);
+
+ const nodeIds = Object.keys(state.nodes);
+ const lines = [
+ `Configuration: loaded (${config.timezone})`,
+ `Controller storage: ${storageExists ? `present at ${config.storagePath}` : "NOT INITIALIZED"}`,
+ `Commissioned nodes: ${nodeIds.length > 0 ? nodeIds.join(", ") : "none recorded"}`,
+ `Host NTP synced: ${ntpSynchronized ? "yes" : "no (or not determinable on this host)"}`,
+ `Current UTC offset: ${formatUtcOffset(offsetSeconds)}`,
+ `Next DST transition: ${
+ transition
+ ? `${transition.at.toISOString()} (${formatUtcOffset(transition.offsetBeforeSeconds)} -> ${formatUtcOffset(
+ transition.offsetAfterSeconds,
+ )})`
+ : "none within 400 days"
+ }`,
+ `Matter time now: ${currentMatterUtcMicroseconds()} us since 2000-01-01T00:00:00Z`,
+ ];
+ for (const [nodeId, perNode] of Object.entries(state.nodes)) {
+ lines.push(`Node ${nodeId}:`);
+ lines.push(` Last connection: ${perNode.lastSuccessfulConnection ?? "never"}`);
+ lines.push(` Last successful sync: ${perNode.lastSuccessfulSync ?? "never"}`);
+ lines.push(` Last attempted sync: ${perNode.lastAttemptedSync ?? "never"}`);
+ lines.push(` Most recent error: ${perNode.lastError ?? "none"}`);
+ }
+ process.stdout.write(lines.join("\n") + "\n");
+ return 0;
+}
+
+async function runFabrics(
+ config: Config,
+ requestedNode: bigint | undefined,
+ removeArgument: string | undefined,
+): Promise<number> {
+ let removeIndex: number | undefined;
+ if (removeArgument !== undefined) {
+ removeIndex = Number(removeArgument);
+ if (!Number.isInteger(removeIndex) || removeIndex < 1) {
+ process.stderr.write(`--remove requires a positive fabric index (got "${removeArgument}")\n`);
+ return 2;
+ }
+ }
+
+ const controller = await startController(config);
+ try {
+ const nodeId = resolveSingleNode(controller, requestedNode);
+ const node = await connectDevice(controller, nodeId);
+
+ if (removeIndex !== undefined) {
+ await removeFabricByIndex(controller, node, removeIndex);
+ }
+
+ const table = await readFabricTable(controller, node);
+ process.stdout.write(formatFabricTable(nodeId, table) + "\n");
+ return 0;
+ } finally {
+ await controller.close();
+ }
+}
+
+async function runDecommission(config: Config, requestedNode: bigint | undefined): Promise<number> {
+ const controller = await startController(config);
+ try {
+ const nodeId = resolveSingleNode(controller, requestedNode);
+ const node = await connectDevice(controller, nodeId);
+
+ log.info(`Decommissioning: removing this controller's fabric from node ${nodeId}`);
+ await node.decommission();
+
+ removeNodeState(serviceStatePath(config.storagePath), nodeId);
+ const remaining = commissionedNodeIds(controller);
+ process.stdout.write(
+ [
+ `Device ${nodeId} removed this controller's fabric; its primary ecosystem is untouched.`,
+ remaining.length > 0
+ ? `${remaining.length} device${remaining.length === 1 ? "" : "s"} remain commissioned: ${remaining.join(", ")}.`
+ : `No devices remain commissioned. Local controller identity remains in ` +
+ `${config.storagePath}; deleting that directory is now safe, or keep it to reuse ` +
+ `the same identity for future commissioning.`,
+ "",
+ ].join("\n"),
+ );
+ return 0;
+ } finally {
+ await controller.close();
+ }
+}
+
+async function promptForPairingCode(): Promise<string> {
+ if (!process.stdin.isTTY) {
+ throw new Error("No pairing code given. Pass --code <pairing-code> when not running interactively.");
+ }
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
+ try {
+ const answer = await new Promise<string>(resolve =>
+ readline.question("Matter pairing code (from the primary ecosystem's pairing mode): ", resolve),
+ );
+ return answer.trim();
+ } finally {
+ readline.close();
+ }
+}
+
+main()
+ .then(code => {
+ process.exitCode = code;
+ })
+ .catch(cause => {
+ log.error("Fatal error", cause);
+ if (!(cause instanceof Error)) {
+ process.stderr.write(describeError(cause) + "\n");
+ }
+ process.exitCode = 1;
+ });
diff --git a/src/commissioning.ts b/src/commissioning.ts
new file mode 100644
index 0000000..b4edf9b
--- /dev/null
+++ b/src/commissioning.ts
@@ -0,0 +1,101 @@
+import { ManualPairingCodeCodec, QrPairingCodeCodec } from "@matter/main/types";
+import { GeneralCommissioning } from "@matter/main/clusters/general-commissioning";
+import type { CommissioningController, NodeCommissioningOptions } from "@project-chip/matter.js";
+import type { Config } from "./config.js";
+import { Log } from "./logging.js";
+import { serviceStatePath, updateNodeState } from "./state.js";
+
+const log = new Log("commissioning");
+
+export interface CommissioningResult {
+ nodeId: bigint;
+}
+
+interface ParsedPairingCode {
+ passcode: number;
+ shortDiscriminator?: number;
+ longDiscriminator?: number;
+}
+
+/**
+ * Parses an Apple-generated Matter multi-admin pairing code. Accepts the
+ * 11/21-digit manual code (with or without hyphens) or an "MT:" QR payload.
+ * The code is used only in memory and never logged or stored.
+ */
+export function parsePairingCode(code: string): ParsedPairingCode {
+ const trimmed = code.trim();
+ if (trimmed.length === 0) {
+ throw new Error("Pairing code is empty");
+ }
+
+ if (trimmed.toUpperCase().startsWith("MT:")) {
+ const [payload] = QrPairingCodeCodec.decode(trimmed);
+ if (payload === undefined) {
+ throw new Error("QR pairing code contains no payload");
+ }
+ return { passcode: payload.passcode, longDiscriminator: payload.discriminator };
+ }
+
+ const digits = trimmed.replace(/[-\s]/g, "");
+ if (!/^\d{11}$|^\d{21}$/.test(digits)) {
+ throw new Error("Manual pairing code must be 11 or 21 digits");
+ }
+ const decoded = ManualPairingCodeCodec.decode(digits);
+ return {
+ passcode: decoded.passcode,
+ shortDiscriminator: decoded.shortDiscriminator,
+ longDiscriminator: decoded.discriminator,
+ };
+}
+
+/**
+ * Commissions a device as an additional Matter administrator using on-network
+ * (IP) discovery only. The device stays joined to its existing primary fabric
+ * (e.g. Apple Home); no Thread credentials are involved.
+ *
+ * Any number of devices may be commissioned, one pairing code each; a device
+ * that is already on this fabric rejects the attempt itself (fabric
+ * conflict), so duplicates cannot occur.
+ */
+export async function commissionDevice(
+ controller: CommissioningController,
+ config: Config,
+ pairingCode: string,
+): Promise<CommissioningResult> {
+ const existing = controller.getCommissionedNodes();
+ if (existing.length > 0) {
+ log.info(
+ `${existing.length} node${existing.length === 1 ? "" : "s"} already commissioned; ` +
+ `adding another device to the same fabric.`,
+ );
+ }
+
+ const parsed = parsePairingCode(pairingCode);
+ const identifierData =
+ parsed.longDiscriminator !== undefined
+ ? { longDiscriminator: parsed.longDiscriminator }
+ : parsed.shortDiscriminator !== undefined
+ ? { shortDiscriminator: parsed.shortDiscriminator }
+ : {};
+
+ log.info("Discovering commissionable device on the IP network (_matterc._udp)...");
+
+ const options: NodeCommissioningOptions = {
+ commissioning: {
+ regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
+ regulatoryCountryCode: "XX",
+ },
+ discovery: {
+ identifierData,
+ discoveryCapabilities: { onIpNetwork: true },
+ },
+ passcode: parsed.passcode,
+ };
+
+ const nodeId = await controller.commissionNode(options, { connectNodeAfterCommissioning: false });
+ const nodeIdBigint = BigInt(nodeId);
+
+ updateNodeState(serviceStatePath(config.storagePath), nodeIdBigint, {});
+ log.info(`Commissioning complete: node ${nodeIdBigint}`);
+ return { nodeId: nodeIdBigint };
+}
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..14dbd4f
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,99 @@
+import { readFileSync } from "node:fs";
+import { isLogLevel, type LogLevel } from "./logging.js";
+import { isValidTimeZone } from "./timezone.js";
+
+export const DEFAULT_CONFIG_PATH = "/etc/mattertimesync/config.json";
+
+export interface Config {
+ /** Directory holding persistent Matter fabric state and service state. Contains secrets. */
+ storagePath: string;
+ /** IANA time-zone name, e.g. "America/Chicago". Never a fixed UTC offset. */
+ timezone: string;
+ logLevel: LogLevel;
+}
+
+export class ConfigError extends Error {}
+
+const DEFAULTS = {
+ timezone: "America/Chicago",
+ logLevel: "info" as LogLevel,
+};
+
+export function loadConfig(path: string): Config {
+ let raw: string;
+ try {
+ raw = readFileSync(path, "utf8");
+ } catch (cause) {
+ throw new ConfigError(
+ `Cannot read configuration file ${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
+ );
+ }
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(raw);
+ } catch (cause) {
+ throw new ConfigError(
+ `Configuration file ${path} is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`,
+ );
+ }
+
+ return validateConfig(parsed, path);
+}
+
+export function validateConfig(parsed: unknown, source = "configuration"): Config {
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
+ throw new ConfigError(`${source}: top-level value must be a JSON object`);
+ }
+ const record = parsed as Record<string, unknown>;
+
+ // Device membership lives in controller storage, not the configuration, so
+ // there is deliberately no nodeId field; commands take --node instead.
+ const known = new Set(["storagePath", "timezone", "logLevel"]);
+ for (const key of Object.keys(record)) {
+ if (!known.has(key)) {
+ throw new ConfigError(`${source}: unknown field "${key}"`);
+ }
+ }
+
+ const storagePath = record["storagePath"];
+ if (typeof storagePath !== "string" || storagePath.length === 0) {
+ throw new ConfigError(`${source}: "storagePath" is required and must be a non-empty string`);
+ }
+
+ const timezone = record["timezone"] === undefined ? DEFAULTS.timezone : record["timezone"];
+ if (typeof timezone !== "string" || !isValidTimeZone(timezone)) {
+ throw new ConfigError(
+ `${source}: "timezone" must be a valid IANA time-zone name (got ${JSON.stringify(timezone)})`,
+ );
+ }
+
+ const logLevel = record["logLevel"] === undefined ? DEFAULTS.logLevel : record["logLevel"];
+ if (!isLogLevel(logLevel)) {
+ throw new ConfigError(`${source}: "logLevel" must be one of debug, info, warn, error`);
+ }
+
+ return { storagePath, timezone, logLevel };
+}
+
+/**
+ * Parses a node ID given on the command line (--node). Node IDs are 64-bit
+ * values; they are carried as decimal strings so they never pass through a
+ * lossy JavaScript number.
+ */
+export function parseNodeId(value: unknown, source = "--node"): bigint {
+ if (typeof value === "string" && /^\d+$/.test(value)) {
+ return toNodeId(BigInt(value), source);
+ }
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
+ return toNodeId(BigInt(value), source);
+ }
+ throw new ConfigError(`${source}: node ID must be a decimal string; got ${JSON.stringify(value)}`);
+}
+
+function toNodeId(value: bigint, source: string): bigint {
+ if (value < 0n || value >= 1n << 64n) {
+ throw new ConfigError(`${source}: node ID must fit in an unsigned 64-bit integer`);
+ }
+ return value;
+}
diff --git a/src/controller.ts b/src/controller.ts
new file mode 100644
index 0000000..b4d27f3
--- /dev/null
+++ b/src/controller.ts
@@ -0,0 +1,96 @@
+import { mkdirSync } from "node:fs";
+import { Environment, StorageService } from "@matter/main";
+import { CommissioningController } from "@project-chip/matter.js";
+import type { Config } from "./config.js";
+import { Log, type LogLevel } from "./logging.js";
+
+const log = new Log("controller");
+
+/**
+ * Fabric label shown to other Matter administrators (e.g. in Apple Home's
+ * connected-services list). Maximum 32 characters.
+ */
+const FABRIC_LABEL = "mattertimesync";
+
+/** Storage namespace under the storage path; also the controller's unique environment id. */
+const CONTROLLER_ID = "mattertimesync";
+
+/**
+ * matter.js library log level derived from the service log level. At service
+ * level "info" the library is kept at "warn" so protocol internals (discovery,
+ * session management) only appear when debugging.
+ */
+function matterLogLevel(serviceLevel: LogLevel): string {
+ return serviceLevel === "debug" ? "debug" : serviceLevel === "info" ? "warn" : serviceLevel;
+}
+
+/**
+ * Creates and starts the Matter controller with persistent storage rooted at
+ * the configured storage path. The fabric identity (root certificate, keys,
+ * node allocation state) persists across restarts; a fabric is only generated
+ * on the very first start.
+ */
+export async function startController(config: Config): Promise<CommissioningController> {
+ mkdirSync(config.storagePath, { recursive: true, mode: 0o700 });
+
+ const environment = Environment.default;
+ environment.vars.set("storage.path", config.storagePath);
+ environment.vars.set("log.level", matterLogLevel(config.logLevel));
+
+ const storage = environment.get(StorageService);
+ log.debug(`Matter storage location: ${storage.location}`);
+
+ const controller = new CommissioningController({
+ environment: { environment, id: CONTROLLER_ID },
+ autoConnect: false,
+ adminFabricLabel: FABRIC_LABEL,
+ });
+
+ await controller.start();
+ const commissioned = controller.getCommissionedNodes();
+ log.info(
+ `Controller started (fabric ${controller.fabric.fabricId}, ${commissioned.length} commissioned node${
+ commissioned.length === 1 ? "" : "s"
+ })`,
+ );
+ return controller;
+}
+
+/** All commissioned node IDs in controller storage. */
+export function commissionedNodeIds(controller: CommissioningController): bigint[] {
+ return controller.getCommissionedNodes().map(id => BigInt(id));
+}
+
+/**
+ * Resolves the set of nodes a command operates on: the explicitly requested
+ * one, or every commissioned node.
+ */
+export function resolveTargetNodes(controller: CommissioningController, requested?: bigint): bigint[] {
+ const commissioned = commissionedNodeIds(controller);
+ if (commissioned.length === 0) {
+ throw new Error('No device is commissioned yet. Run "commission" first.');
+ }
+ if (requested === undefined) return commissioned;
+ if (!commissioned.includes(requested)) {
+ throw new Error(
+ `Node ${requested} is not commissioned on this controller ` +
+ `(known nodes: ${commissioned.join(", ")}). Run "nodes" to list them.`,
+ );
+ }
+ return [requested];
+}
+
+/**
+ * Resolves exactly one node for commands that cannot meaningfully operate on
+ * several (inspect, fabrics, decommission). --node is required once more than
+ * one device is commissioned.
+ */
+export function resolveSingleNode(controller: CommissioningController, requested?: bigint): bigint {
+ const targets = resolveTargetNodes(controller, requested);
+ if (targets.length > 1) {
+ throw new Error(
+ `Multiple nodes are commissioned (${targets.join(", ")}); pass --node <id> to choose one.`,
+ );
+ }
+ return targets[0]!;
+}
diff --git a/src/device.ts b/src/device.ts
new file mode 100644
index 0000000..1c0d0ea
--- /dev/null
+++ b/src/device.ts
@@ -0,0 +1,72 @@
+import { NodeId } from "@matter/main";
+import type { CommissioningController } from "@project-chip/matter.js";
+import { NodeStates, type PairedNode } from "@project-chip/matter.js/device";
+import { Log } from "./logging.js";
+
+const log = new Log("device");
+
+export function nodeStateName(state: NodeStates): string {
+ switch (state) {
+ case NodeStates.Connected:
+ return "connected";
+ case NodeStates.Disconnected:
+ return "disconnected";
+ case NodeStates.Reconnecting:
+ return "reconnecting";
+ case NodeStates.WaitingForDeviceDiscovery:
+ return "waiting for device discovery";
+ }
+}
+
+/**
+ * Connects to the commissioned node using operational discovery (DNS-SD by
+ * node identity, never a stored IP address) and waits until the node is fully
+ * initialized from the device.
+ *
+ * The returned PairedNode keeps reconnecting in the background; observe
+ * `events.stateChanged` for connectivity transitions.
+ */
+export async function connectDevice(
+ controller: CommissioningController,
+ nodeId: bigint,
+ timeoutMs = 60_000,
+): Promise<PairedNode> {
+ const node = await controller.getNode(NodeId(nodeId));
+
+ node.events.stateChanged.on(state => {
+ log.debug(`Node ${nodeId} state: ${nodeStateName(state)}`);
+ });
+
+ if (!node.initialized) {
+ log.info(`Connecting to Matter node ${nodeId}...`);
+ node.connect();
+ await waitForInitialization(node, nodeId, timeoutMs);
+ } else if (!node.isConnected) {
+ node.connect();
+ await waitForInitialization(node, nodeId, timeoutMs);
+ }
+
+ log.info(`Connected to Matter node ${nodeId}`);
+ return node;
+}
+
+async function waitForInitialization(node: PairedNode, nodeId: bigint, timeoutMs: number): Promise<void> {
+ let timer: NodeJS.Timeout | undefined;
+ const timeout = new Promise<never>((_, reject) => {
+ timer = setTimeout(
+ () =>
+ reject(
+ new Error(
+ `Timed out after ${Math.round(timeoutMs / 1000)}s waiting for node ${nodeId}. ` +
+ `The device may be offline or unreachable over IPv6.`,
+ ),
+ ),
+ timeoutMs,
+ );
+ });
+ try {
+ await Promise.race([node.events.initializedFromRemote, timeout]);
+ } finally {
+ clearTimeout(timer);
+ }
+}
diff --git a/src/fabrics.ts b/src/fabrics.ts
new file mode 100644
index 0000000..2c4c90e
--- /dev/null
+++ b/src/fabrics.ts
@@ -0,0 +1,153 @@
+import { FabricIndex } from "@matter/main";
+import { OperationalCredentials } from "@matter/main/clusters/operational-credentials";
+import type { CommissioningController } from "@project-chip/matter.js";
+import type { PairedNode } from "@project-chip/matter.js/device";
+import { Log } from "./logging.js";
+
+const log = new Log("fabrics");
+
+/**
+ * Fabric-table management on the device's Operational Credentials cluster.
+ *
+ * Every commissioning writes a fabric entry into the device's limited fabric
+ * table. Entries never expire; identities whose local storage was deleted
+ * leave orphans behind. These helpers make the table visible and allow an
+ * admin (us) to remove stale entries without a factory reset.
+ */
+
+export interface FabricEntry {
+ fabricIndex: number;
+ fabricId: bigint;
+ nodeId: bigint;
+ vendorId: number;
+ label: string;
+ /** True when the entry belongs to this controller's current identity. */
+ isOurs: boolean;
+}
+
+export interface FabricTable {
+ supportedFabrics: number;
+ commissionedFabrics: number;
+ entries: FabricEntry[];
+}
+
+const KNOWN_VENDORS: Record<number, string> = {
+ 0x1349: "Apple",
+ 0x6006: "Google",
+ 0x10e1: "SmartThings",
+ 0x117c: "IKEA",
+};
+
+function vendorName(vendorId: number): string {
+ const known = KNOWN_VENDORS[vendorId];
+ if (known !== undefined) return known;
+ if (vendorId >= 0xfff1 && vendorId <= 0xfff4) return "test vendor";
+ return "unknown vendor";
+}
+
+export async function readFabricTable(
+ controller: CommissioningController,
+ node: PairedNode,
+): Promise<FabricTable> {
+ const client = node.getRootClusterClient(OperationalCredentials.Complete);
+ if (client === undefined) {
+ throw new Error("Operational Credentials cluster not found on the device's root endpoint");
+ }
+
+ // Read from the device with fabric filtering off, so entries from all
+ // fabrics are returned, not just our own.
+ const fabrics = await client.attributes.fabrics.get(true, false);
+ const supportedFabrics = (await client.attributes.supportedFabrics.get(true)) ?? 0;
+ const commissionedFabrics = (await client.attributes.commissionedFabrics.get(true)) ?? 0;
+ if (fabrics === undefined) {
+ throw new Error("Device returned no fabric list");
+ }
+
+ const ourFabric = controller.fabric;
+ const entries = fabrics.map(fabric => ({
+ fabricIndex: Number(fabric.fabricIndex),
+ fabricId: BigInt(fabric.fabricId),
+ nodeId: BigInt(fabric.nodeId),
+ vendorId: Number(fabric.vendorId),
+ label: fabric.label,
+ isOurs: ourFabric.matchesFabricIdAndRootPublicKey(fabric.fabricId, fabric.rootPublicKey),
+ }));
+
+ return { supportedFabrics, commissionedFabrics, entries };
+}
+
+export function formatFabricTable(nodeId: bigint, table: FabricTable): string {
+ const lines: string[] = [];
+ lines.push(
+ `Fabric table on node ${nodeId} (${table.commissionedFabrics} of ${table.supportedFabrics} slots used):`,
+ );
+ for (const entry of table.entries) {
+ const vendor = `0x${entry.vendorId.toString(16).padStart(4, "0")} (${vendorName(entry.vendorId)})`;
+ const marker = entry.isOurs ? " [this controller]" : "";
+ lines.push(
+ ` index ${entry.fabricIndex}: label ${JSON.stringify(entry.label)} vendor ${vendor} ` +
+ `fabricId ${entry.fabricId} nodeId ${entry.nodeId}${marker}`,
+ );
+ }
+ const strays = findLikelyStrays(table);
+ if (strays.length > 0) {
+ lines.push("");
+ lines.push(
+ `Likely stale entries (our label, but not our current identity): ` +
+ `${strays.map(entry => `index ${entry.fabricIndex}`).join(", ")}`,
+ );
+ lines.push(`Remove one with: mattertimesync fabrics --remove <index>`);
+ }
+ return lines.join("\n");
+}
+
+/**
+ * Entries carrying our fabric label but not our current identity: orphans
+ * from a commissioning whose local storage was deleted or replaced.
+ */
+export function findLikelyStrays(table: FabricTable): FabricEntry[] {
+ const ourLabel = table.entries.find(entry => entry.isOurs)?.label;
+ return table.entries.filter(entry => !entry.isOurs && ourLabel !== undefined && entry.label === ourLabel);
+}
+
+/**
+ * Removes another fabric's entry from the device. Refuses to remove our own
+ * entry; `decommission` is the correct path for that, because it also cleans
+ * up local controller state.
+ */
+export async function removeFabricByIndex(
+ controller: CommissioningController,
+ node: PairedNode,
+ fabricIndex: number,
+): Promise<void> {
+ const table = await readFabricTable(controller, node);
+ const entry = table.entries.find(candidate => candidate.fabricIndex === fabricIndex);
+ if (entry === undefined) {
+ throw new Error(
+ `No fabric with index ${fabricIndex} on the device ` +
+ `(present: ${table.entries.map(candidate => candidate.fabricIndex).join(", ")})`,
+ );
+ }
+ if (entry.isOurs) {
+ throw new Error(
+ `Fabric index ${fabricIndex} is this controller's own entry. ` +
+ `Use "decommission" instead, so local state is cleaned up too.`,
+ );
+ }
+
+ const client = node.getRootClusterClient(OperationalCredentials.Complete);
+ if (client === undefined) {
+ throw new Error("Operational Credentials cluster not found on the device's root endpoint");
+ }
+
+ log.info(`Removing fabric index ${fabricIndex} (label ${JSON.stringify(entry.label)}) from the device`);
+ const response = await client.commands.removeFabric({ fabricIndex: FabricIndex(fabricIndex) });
+ if (response.statusCode !== OperationalCredentials.NodeOperationalCertStatus.Ok) {
+ throw new Error(
+ `Device rejected RemoveFabric for index ${fabricIndex}: ` +
+ `status ${OperationalCredentials.NodeOperationalCertStatus[response.statusCode] ?? response.statusCode}` +
+ `${response.debugText ? ` (${response.debugText})` : ""}`,
+ );
+ }
+ log.info(`Fabric index ${fabricIndex} removed`);
+}
diff --git a/src/inspection.ts b/src/inspection.ts
new file mode 100644
index 0000000..b022b60
--- /dev/null
+++ b/src/inspection.ts
@@ -0,0 +1,272 @@
+import { ClusterId } from "@matter/main";
+import { SupportedAttributeClient } from "@project-chip/matter.js/cluster";
+import type { Endpoint, PairedNode } from "@project-chip/matter.js/device";
+import { Log } from "./logging.js";
+
+const log = new Log("inspection");
+
+const TIME_SYNC_CLUSTER_ID = ClusterId(0x38);
+/** Standard cluster IDs are <= 0x7fff; anything above is manufacturer-specific. */
+const LAST_STANDARD_CLUSTER_ID = 0x7fff;
+
+export interface ClusterSummary {
+ id: string;
+ name: string;
+ revision: number;
+ isUnknown: boolean;
+ vendorSpecific: boolean;
+ supportedFeatures: Record<string, boolean>;
+}
+
+export interface EndpointSummary {
+ endpointId: number;
+ name: string;
+ deviceTypes: { name: string; code: string; revision: number }[];
+ serverClusters: ClusterSummary[];
+ /** Client-side cluster IDs from the endpoint's descriptor. */
+ clientClusters: string[];
+}
+
+export interface TimeSyncAttribute {
+ id: string;
+ name: string;
+ value: unknown;
+}
+
+export interface TimeSyncSummary {
+ endpointId: number;
+ clusterRevision: number;
+ supportedFeatures: Record<string, boolean>;
+ featureMap: unknown;
+ supportedCommands: Record<string, boolean>;
+ attributes: TimeSyncAttribute[];
+}
+
+export interface InspectionReport {
+ nodeId: string;
+ basicInformation: {
+ vendorName: string;
+ vendorId: number;
+ productName: string;
+ productId: number;
+ hardwareVersion: string;
+ softwareVersion: string;
+ serialNumber?: string;
+ uniqueId?: string;
+ } | null;
+ endpoints: EndpointSummary[];
+ timeSynchronization: TimeSyncSummary | null;
+ vendorSpecificClusters: { endpointId: number; clusterId: string; name: string }[];
+}
+
+/** Commands whose availability drives the capability-based sync stages. */
+const TIME_SYNC_COMMANDS = [
+ "setUtcTime",
+ "setTrustedTimeSource",
+ "setTimeZone",
+ "setDstOffset",
+ "setDefaultNtp",
+] as const;
+
+export async function inspectNode(node: PairedNode): Promise<InspectionReport> {
+ const endpoints = collectEndpoints(node);
+
+ const endpointSummaries = endpoints.map(endpoint => summarizeEndpoint(endpoint));
+
+ const info = node.basicInformation;
+ const basicInformation = info
+ ? {
+ vendorName: info.vendorName,
+ vendorId: Number(info.vendorId),
+ productName: info.productName,
+ productId: info.productId,
+ hardwareVersion: info.hardwareVersionString,
+ softwareVersion: info.softwareVersionString,
+ serialNumber: info.serialNumber,
+ uniqueId: info.uniqueId,
+ }
+ : null;
+
+ const timeSynchronization = await summarizeTimeSync(endpoints);
+ if (timeSynchronization === null) {
+ log.warn("Time Synchronization cluster not found on any endpoint");
+ } else {
+ log.info(`Time Synchronization cluster found on endpoint ${timeSynchronization.endpointId}`);
+ }
+
+ const vendorSpecificClusters = endpointSummaries.flatMap(endpoint =>
+ endpoint.serverClusters
+ .filter(cluster => cluster.vendorSpecific)
+ .map(cluster => ({ endpointId: endpoint.endpointId, clusterId: cluster.id, name: cluster.name })),
+ );
+
+ return {
+ nodeId: BigInt(node.nodeId).toString(),
+ basicInformation,
+ endpoints: endpointSummaries,
+ timeSynchronization,
+ vendorSpecificClusters,
+ };
+}
+
+function collectEndpoints(node: PairedNode): Endpoint[] {
+ const seen = new Map<number, Endpoint>();
+ const root = node.getRootEndpoint();
+ if (root?.number !== undefined) seen.set(root.number, root);
+ for (const [number, endpoint] of node.parts) {
+ if (!seen.has(number)) seen.set(number, endpoint);
+ }
+ return [...seen.entries()].sort(([a], [b]) => a - b).map(([, endpoint]) => endpoint);
+}
+
+function summarizeEndpoint(endpoint: Endpoint): EndpointSummary {
+ const serverClusters = endpoint.getAllClusterClients().map(client => ({
+ id: formatClusterId(client.id),
+ name: client.name,
+ revision: client.revision,
+ isUnknown: client.isUnknown,
+ vendorSpecific: client.id > LAST_STANDARD_CLUSTER_ID,
+ supportedFeatures: Object.fromEntries(
+ Object.entries(client.supportedFeatures ?? {}).filter(([, enabled]) => typeof enabled === "boolean"),
+ ) as Record<string, boolean>,
+ }));
+
+ let clientClusters: string[] = [];
+ try {
+ clientClusters = (endpoint.state.descriptor.clientList ?? []).map(id => formatClusterId(id));
+ } catch {
+ // Descriptor state unavailable; leave the client list empty.
+ }
+
+ return {
+ endpointId: endpoint.number ?? -1,
+ name: endpoint.name,
+ deviceTypes: endpoint.getDeviceTypes().map(type => ({
+ name: type.name,
+ code: `0x${type.code.toString(16).padStart(4, "0")}`,
+ revision: type.revision,
+ })),
+ serverClusters,
+ clientClusters,
+ };
+}
+
+async function summarizeTimeSync(endpoints: Endpoint[]): Promise<TimeSyncSummary | null> {
+ for (const endpoint of endpoints) {
+ const client = endpoint.getClusterClientById(TIME_SYNC_CLUSTER_ID);
+ if (client === undefined) continue;
+
+ const attributes: TimeSyncAttribute[] = [];
+ for (const [name, attribute] of Object.entries(client.attributes)) {
+ if (!(attribute instanceof SupportedAttributeClient)) continue;
+ let value: unknown;
+ try {
+ value = attribute.getLocal();
+ if (value === undefined) value = await attribute.get();
+ } catch (cause) {
+ value = `<read failed: ${cause instanceof Error ? cause.message : String(cause)}>`;
+ }
+ attributes.push({ id: `0x${attribute.id.toString(16)}`, name, value });
+ }
+
+ const supportedCommands = Object.fromEntries(
+ TIME_SYNC_COMMANDS.map(command => [command, client.isCommandSupportedByName(command)]),
+ );
+
+ const featureMap = attributes.find(attribute => attribute.name === "featureMap")?.value ?? null;
+
+ return {
+ endpointId: endpoint.number ?? -1,
+ clusterRevision: client.revision,
+ supportedFeatures: Object.fromEntries(
+ Object.entries(client.supportedFeatures ?? {}).filter(([, enabled]) => typeof enabled === "boolean"),
+ ) as Record<string, boolean>,
+ featureMap,
+ supportedCommands,
+ attributes,
+ };
+ }
+ return null;
+}
+
+function formatClusterId(id: number): string {
+ return `0x${Number(id).toString(16).padStart(4, "0")}`;
+}
+
+/** JSON output with bigints rendered as decimal strings. */
+export function inspectionToJson(report: InspectionReport): string {
+ return JSON.stringify(report, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2);
+}
+
+export function formatInspection(report: InspectionReport): string {
+ const lines: string[] = [];
+ lines.push(`Node ${report.nodeId}`);
+
+ if (report.basicInformation) {
+ const info = report.basicInformation;
+ lines.push(` Vendor: ${info.vendorName} (0x${info.vendorId.toString(16).padStart(4, "0")})`);
+ lines.push(` Product: ${info.productName} (0x${info.productId.toString(16).padStart(4, "0")})`);
+ lines.push(` Hardware: ${info.hardwareVersion} Software: ${info.softwareVersion}`);
+ if (info.serialNumber) lines.push(` Serial: ${info.serialNumber}`);
+ }
+
+ for (const endpoint of report.endpoints) {
+ lines.push("");
+ const types = endpoint.deviceTypes.map(t => `${t.name} (${t.code}, rev ${t.revision})`).join(", ");
+ lines.push(`Endpoint ${endpoint.endpointId}: ${types}`);
+ if (endpoint.serverClusters.length > 0) {
+ lines.push(" Server clusters:");
+ for (const cluster of endpoint.serverClusters) {
+ const features = Object.entries(cluster.supportedFeatures)
+ .filter(([, enabled]) => enabled)
+ .map(([name]) => name);
+ const notes = [
+ `rev ${cluster.revision}`,
+ ...(features.length > 0 ? [`features: ${features.join(", ")}`] : []),
+ ...(cluster.vendorSpecific ? ["vendor-specific"] : []),
+ ...(cluster.isUnknown ? ["unknown to matter.js"] : []),
+ ];
+ lines.push(` ${cluster.id} ${cluster.name} (${notes.join("; ")})`);
+ }
+ }
+ if (endpoint.clientClusters.length > 0) {
+ lines.push(` Client clusters: ${endpoint.clientClusters.join(", ")}`);
+ }
+ }
+
+ lines.push("");
+ if (report.timeSynchronization) {
+ const ts = report.timeSynchronization;
+ lines.push(`Time Synchronization cluster (endpoint ${ts.endpointId}, revision ${ts.clusterRevision})`);
+ const features = Object.entries(ts.supportedFeatures)
+ .filter(([, enabled]) => enabled)
+ .map(([name]) => name);
+ lines.push(` Features: ${features.length > 0 ? features.join(", ") : "(none)"}`);
+ lines.push(" Commands:");
+ for (const [command, supported] of Object.entries(ts.supportedCommands)) {
+ lines.push(` ${command}: ${supported ? "supported" : "not supported"}`);
+ }
+ lines.push(" Attributes:");
+ for (const attribute of ts.attributes) {
+ lines.push(` ${attribute.name} (${attribute.id}): ${renderValue(attribute.value)}`);
+ }
+ } else {
+ lines.push("Time Synchronization cluster: NOT FOUND");
+ lines.push(" This device cannot be synchronized through the standard Matter mechanism.");
+ }
+
+ if (report.vendorSpecificClusters.length > 0) {
+ lines.push("");
+ lines.push("Vendor-specific clusters:");
+ for (const cluster of report.vendorSpecificClusters) {
+ lines.push(` endpoint ${cluster.endpointId}: ${cluster.clusterId} ${cluster.name}`);
+ }
+ }
+
+ return lines.join("\n");
+}
+
+function renderValue(value: unknown): string {
+ if (value === undefined) return "<not read>";
+ return JSON.stringify(value, (_key, inner) => (typeof inner === "bigint" ? inner.toString() : inner));
+}
diff --git a/src/logging.ts b/src/logging.ts
new file mode 100644
index 0000000..c8c75d7
--- /dev/null
+++ b/src/logging.ts
@@ -0,0 +1,71 @@
+/**
+ * Structured, level-filtered logging for the service's own messages.
+ *
+ * matter.js library logging is configured separately (see controller.ts); this
+ * logger covers service-level events in the format:
+ *
+ * 2026-07-26T20:00:00.100Z INFO controller Connected to Matter node 1
+ */
+
+export const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
+export type LogLevel = (typeof LOG_LEVELS)[number];
+
+export function isLogLevel(value: unknown): value is LogLevel {
+ return typeof value === "string" && (LOG_LEVELS as readonly string[]).includes(value);
+}
+
+const LEVEL_RANK: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
+const LEVEL_LABEL: Record<LogLevel, string> = {
+ debug: "DEBUG",
+ info: "INFO ",
+ warn: "WARN ",
+ error: "ERROR",
+};
+
+let globalLevel: LogLevel = "info";
+
+export function setLogLevel(level: LogLevel): void {
+ globalLevel = level;
+}
+
+export function getLogLevel(): LogLevel {
+ return globalLevel;
+}
+
+function write(level: LogLevel, module: string, message: string): void {
+ if (LEVEL_RANK[level] < LEVEL_RANK[globalLevel]) return;
+ const line = `${new Date().toISOString()} ${LEVEL_LABEL[level]} ${module} ${message}`;
+ if (level === "error" || level === "warn") {
+ process.stderr.write(line + "\n");
+ } else {
+ process.stdout.write(line + "\n");
+ }
+}
+
+/** A logger bound to a module name (e.g. "controller", "sync"). */
+export class Log {
+ constructor(private readonly module: string) {}
+
+ debug(message: string): void {
+ write("debug", this.module, message);
+ }
+
+ info(message: string): void {
+ write("info", this.module, message);
+ }
+
+ warn(message: string): void {
+ write("warn", this.module, message);
+ }
+
+ error(message: string, cause?: unknown): void {
+ write("error", this.module, cause === undefined ? message : `${message}: ${describeError(cause)}`);
+ }
+}
+
+export function describeError(cause: unknown): string {
+ if (cause instanceof Error) {
+ return cause.message;
+ }
+ return String(cause);
+}
diff --git a/src/state.ts b/src/state.ts
new file mode 100644
index 0000000..c5621fe
--- /dev/null
+++ b/src/state.ts
@@ -0,0 +1,90 @@
+import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+
+/**
+ * Small service-state file kept next to the Matter fabric storage, with one
+ * entry per commissioned node (keyed by node ID as a decimal string). Written
+ * atomically (temp file + rename) so a power loss mid-write never corrupts it.
+ */
+export interface NodeState {
+ lastSuccessfulConnection: string | null;
+ lastSuccessfulSync: string | null;
+ lastAttemptedSync: string | null;
+ lastError: string | null;
+}
+
+export interface ServiceState {
+ nodes: Record<string, NodeState>;
+}
+
+export const EMPTY_NODE_STATE: NodeState = {
+ lastSuccessfulConnection: null,
+ lastSuccessfulSync: null,
+ lastAttemptedSync: null,
+ lastError: null,
+};
+
+export function serviceStatePath(storagePath: string): string {
+ return join(storagePath, "service-state.json");
+}
+
+export function readServiceState(path: string): ServiceState {
+ let raw: string;
+ try {
+ raw = readFileSync(path, "utf8");
+ } catch {
+ return { nodes: {} };
+ }
+ try {
+ const parsed = JSON.parse(raw) as Partial<ServiceState>;
+ const nodes: Record<string, NodeState> = {};
+ if (typeof parsed.nodes === "object" && parsed.nodes !== null) {
+ for (const [nodeId, value] of Object.entries(parsed.nodes)) {
+ if (!/^\d+$/.test(nodeId) || typeof value !== "object" || value === null) continue;
+ const entry = value as Partial<NodeState>;
+ nodes[nodeId] = {
+ lastSuccessfulConnection: stringOrNull(entry.lastSuccessfulConnection),
+ lastSuccessfulSync: stringOrNull(entry.lastSuccessfulSync),
+ lastAttemptedSync: stringOrNull(entry.lastAttemptedSync),
+ lastError: stringOrNull(entry.lastError),
+ };
+ }
+ }
+ return { nodes };
+ } catch {
+ // A corrupt state file is not fatal; it only holds status metadata.
+ return { nodes: {} };
+ }
+}
+
+export function writeServiceState(path: string, state: ServiceState): void {
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
+ const temp = `${path}.tmp`;
+ writeFileSync(temp, JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
+ renameSync(temp, path);
+}
+
+export function nodeState(state: ServiceState, nodeId: bigint): NodeState {
+ return state.nodes[nodeId.toString()] ?? { ...EMPTY_NODE_STATE };
+}
+
+/** Merges a patch into one node's entry and persists the result. */
+export function updateNodeState(path: string, nodeId: bigint, patch: Partial<NodeState>): ServiceState {
+ const state = readServiceState(path);
+ const key = nodeId.toString();
+ state.nodes[key] = { ...(state.nodes[key] ?? EMPTY_NODE_STATE), ...patch };
+ writeServiceState(path, state);
+ return state;
+}
+
+/** Drops one node's entry (after decommissioning) and persists the result. */
+export function removeNodeState(path: string, nodeId: bigint): ServiceState {
+ const state = readServiceState(path);
+ delete state.nodes[nodeId.toString()];
+ writeServiceState(path, state);
+ return state;
+}
+
+function stringOrNull(value: unknown): string | null {
+ return typeof value === "string" ? value : null;
+}
diff --git a/src/sync.ts b/src/sync.ts
new file mode 100644
index 0000000..ec3577c
--- /dev/null
+++ b/src/sync.ts
@@ -0,0 +1,120 @@
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+/**
+ * Matter epoch conversion.
+ *
+ * Matter UTC time is microseconds since 2000-01-01T00:00:00Z (the "Matter
+ * epoch"), not the Unix epoch. All arithmetic stays in bigint; the full
+ * microsecond timestamp must never pass through a JavaScript number.
+ *
+ * The Time Synchronization write commands (SetUtcTime, SetTimeZone,
+ * SetDstOffset) are intentionally NOT implemented yet: per the project plan
+ * they are Phase 5/6 work that must be driven by the real device's inspected
+ * capabilities. Run `inspect` against the hardware first.
+ */
+
+export const MATTER_EPOCH_UNIX_SECONDS = 946_684_800n;
+
+/** Converts a Unix-epoch timestamp in milliseconds to Matter-epoch microseconds. */
+export function unixMillisToMatterMicros(unixMilliseconds: bigint): bigint {
+ return unixMilliseconds * 1_000n - MATTER_EPOCH_UNIX_SECONDS * 1_000_000n;
+}
+
+/** Converts Matter-epoch microseconds back to Unix-epoch milliseconds (truncating). */
+export function matterMicrosToUnixMillis(matterMicroseconds: bigint): bigint {
+ return (matterMicroseconds + MATTER_EPOCH_UNIX_SECONDS * 1_000_000n) / 1_000n;
+}
+
+/** Current time as Matter-epoch microseconds. */
+export function currentMatterUtcMicroseconds(now: () => number = Date.now): bigint {
+ return unixMillisToMatterMicros(BigInt(now()));
+}
+
+/** Renders Matter-epoch microseconds as an ISO-8601 UTC instant (millisecond precision). */
+export function formatMatterMicros(matterMicroseconds: bigint): string {
+ return new Date(Number(matterMicrosToUnixMillis(matterMicroseconds))).toISOString();
+}
+
+/**
+ * Comparison of the device's reported clock against the time we are about to
+ * set, for the sync report ("was X, set to Y, delta Z").
+ */
+export interface ClockComparison {
+ /** Device time before sync as ISO-8601, or null if its clock was unset. */
+ deviceTime: string | null;
+ /** The time being written, as ISO-8601. */
+ hostTime: string;
+ /** deviceTime - hostTime in microseconds; null when the device clock was unset. */
+ deltaMicroseconds: bigint | null;
+ /** Human-readable summary, e.g. "device clock was 3.2s behind". */
+ description: string;
+}
+
+/**
+ * Compares the device's utcTime attribute (null when the device lost its
+ * clock, e.g. after a power outage) against the host time being written.
+ */
+export function compareDeviceClock(deviceMicros: bigint | null, hostMicros: bigint): ClockComparison {
+ const hostTime = formatMatterMicros(hostMicros);
+ if (deviceMicros === null) {
+ return {
+ deviceTime: null,
+ hostTime,
+ deltaMicroseconds: null,
+ description: "device clock was unset",
+ };
+ }
+ const delta = deviceMicros - hostMicros;
+ const magnitude = delta < 0n ? -delta : delta;
+ const description =
+ magnitude < 1_000_000n
+ ? `device clock was within 1s of host time (${formatDurationMicros(magnitude)} ${
+ delta < 0n ? "behind" : "ahead"
+ })`
+ : `device clock was ${formatDurationMicros(magnitude)} ${delta < 0n ? "behind" : "ahead"}`;
+ return { deviceTime: formatMatterMicros(deviceMicros), hostTime, deltaMicroseconds: delta, description };
+}
+
+/** Formats a non-negative duration in microseconds as a compact human string. */
+export function formatDurationMicros(microseconds: bigint): string {
+ if (microseconds < 0n) throw new Error("duration must be non-negative");
+ if (microseconds < 1_000n) return `${microseconds}us`;
+ if (microseconds < 1_000_000n) return `${microseconds / 1_000n}ms`;
+
+ const totalSeconds = microseconds / 1_000_000n;
+ if (totalSeconds < 60n) {
+ const tenths = (microseconds % 1_000_000n) / 100_000n;
+ return tenths === 0n ? `${totalSeconds}s` : `${totalSeconds}.${tenths}s`;
+ }
+ const days = totalSeconds / 86_400n;
+ const hours = (totalSeconds % 86_400n) / 3_600n;
+ const minutes = (totalSeconds % 3_600n) / 60n;
+ const seconds = totalSeconds % 60n;
+ const parts: string[] = [];
+ if (days > 0n) parts.push(`${days}d`);
+ if (hours > 0n) parts.push(`${hours}h`);
+ if (minutes > 0n) parts.push(`${minutes}m`);
+ if (seconds > 0n && days === 0n) parts.push(`${seconds}s`);
+ return parts.join(" ");
+}
+
+const execFileAsync = promisify(execFile);
+
+/**
+ * Whether the host clock is NTP-synchronized, via systemd-timesyncd. The
+ * device must never be set from an unsynchronized clock.
+ *
+ * Returns false on hosts without timedatectl (e.g. during development on
+ * macOS) so callers fail safe; pass `assumeSynchronized` explicitly in tests.
+ */
+export async function isHostClockSynchronized(): Promise<boolean> {
+ try {
+ const { stdout } = await execFileAsync("timedatectl", ["show", "-p", "NTPSynchronized", "--value"], {
+ timeout: 5000,
+ });
+ return stdout.trim() === "yes";
+ } catch {
+ return false;
+ }
+}
diff --git a/src/timezone.ts b/src/timezone.ts
new file mode 100644
index 0000000..f4240f4
--- /dev/null
+++ b/src/timezone.ts
@@ -0,0 +1,106 @@
+/**
+ * IANA time-zone helpers.
+ *
+ * Phase 1-4 scope: validation and offset introspection used by config
+ * validation and status output. Matter TimeZone/DSTOffset structure
+ * generation (Phase 6) will build on these helpers once the real device's
+ * Time Synchronization capabilities have been inspected.
+ */
+
+/** Returns true when the host's ICU data recognizes the IANA time-zone name. */
+export function isValidTimeZone(timezone: string): boolean {
+ if (typeof timezone !== "string" || timezone.length === 0) return false;
+ try {
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Current UTC offset of `timezone` at `date`, in seconds.
+ *
+ * Derived from ICU rules, never from a hard-coded offset, so daylight-saving
+ * transitions are honored automatically.
+ */
+export function utcOffsetSeconds(timezone: string, date: Date = new Date()): number {
+ const formatter = new Intl.DateTimeFormat("en-US", {
+ timeZone: timezone,
+ timeZoneName: "longOffset",
+ });
+ const offsetPart = formatter.formatToParts(date).find(part => part.type === "timeZoneName")?.value;
+ if (offsetPart === undefined) {
+ throw new Error(`Unable to determine UTC offset for time zone ${timezone}`);
+ }
+ // Formats look like "GMT-05:00", "GMT+05:30", or plain "GMT" for UTC itself.
+ const match = /^GMT(?:([+-])(\d{2}):(\d{2})(?::(\d{2}))?)?$/.exec(offsetPart);
+ if (!match) {
+ throw new Error(`Unrecognized UTC offset format "${offsetPart}" for time zone ${timezone}`);
+ }
+ if (match[1] === undefined) return 0;
+ const sign = match[1] === "-" ? -1 : 1;
+ const hours = Number(match[2]);
+ const minutes = Number(match[3]);
+ const seconds = match[4] === undefined ? 0 : Number(match[4]);
+ return sign * (hours * 3600 + minutes * 60 + seconds);
+}
+
+/** Formats an offset in seconds as "UTC-05:00" style text for logs and status output. */
+export function formatUtcOffset(offsetSeconds: number): string {
+ const sign = offsetSeconds < 0 ? "-" : "+";
+ const absolute = Math.abs(offsetSeconds);
+ const hours = String(Math.floor(absolute / 3600)).padStart(2, "0");
+ const minutes = String(Math.floor((absolute % 3600) / 60)).padStart(2, "0");
+ return `UTC${sign}${hours}:${minutes}`;
+}
+
+/**
+ * Finds the next instant at which the zone's UTC offset changes, scanning up
+ * to `horizonDays` ahead. Returns null when no transition occurs in the
+ * window (e.g. fixed-offset zones).
+ *
+ * Uses day-granularity scan plus binary search, so it is exact to the second
+ * without iterating minute-by-minute.
+ */
+export function nextOffsetTransition(
+ timezone: string,
+ from: Date = new Date(),
+ horizonDays = 400,
+): { at: Date; offsetBeforeSeconds: number; offsetAfterSeconds: number } | null {
+ const startOffset = utcOffsetSeconds(timezone, from);
+ const dayMs = 24 * 60 * 60 * 1000;
+ // Probe on whole-second boundaries so the binary search converges on the
+ // exact transition instant (transitions occur at whole seconds).
+ const startMs = Math.ceil(from.getTime() / 1000) * 1000;
+
+ let previous = startMs;
+ let changedAtOrBefore: number | null = null;
+ for (let day = 1; day <= horizonDays; day++) {
+ const probe = startMs + day * dayMs;
+ if (utcOffsetSeconds(timezone, new Date(probe)) !== startOffset) {
+ changedAtOrBefore = probe;
+ break;
+ }
+ previous = probe;
+ }
+ if (changedAtOrBefore === null) return null;
+
+ // Binary search the exact transition instant between the last unchanged
+ // probe and the first changed probe.
+ let low = previous;
+ let high = changedAtOrBefore;
+ while (high - low > 1000) {
+ const middle = low + Math.floor((high - low) / 2 / 1000) * 1000;
+ if (utcOffsetSeconds(timezone, new Date(middle)) === startOffset) {
+ low = middle;
+ } else {
+ high = middle;
+ }
+ }
+ return {
+ at: new Date(high),
+ offsetBeforeSeconds: startOffset,
+ offsetAfterSeconds: utcOffsetSeconds(timezone, new Date(high)),
+ };
+}
diff --git a/test/commissioning.test.ts b/test/commissioning.test.ts
new file mode 100644
index 0000000..676a5e1
--- /dev/null
+++ b/test/commissioning.test.ts
@@ -0,0 +1,33 @@
+import { ManualPairingCodeCodec } from "@matter/main/types";
+import { describe, expect, it } from "vitest";
+import { parsePairingCode } from "../src/commissioning.js";
+
+describe("parsePairingCode", () => {
+ const validCode = ManualPairingCodeCodec.encode({ discriminator: 3840, passcode: 20202021 });
+
+ it("parses a valid 11-digit manual code", () => {
+ const parsed = parsePairingCode(validCode);
+ expect(parsed.passcode).toBe(20202021);
+ expect(parsed.shortDiscriminator).toBe(3840 >> 8);
+ });
+
+ it("accepts hyphens and whitespace", () => {
+ const withHyphens = `${validCode.slice(0, 4)}-${validCode.slice(4, 7)}-${validCode.slice(7)}`;
+ expect(parsePairingCode(` ${withHyphens} `).passcode).toBe(20202021);
+ });
+
+ it("rejects empty input", () => {
+ expect(() => parsePairingCode("")).toThrow(/empty/);
+ expect(() => parsePairingCode(" ")).toThrow(/empty/);
+ });
+
+ it("rejects codes of the wrong length", () => {
+ expect(() => parsePairingCode("12345")).toThrow(/11 or 21 digits/);
+ expect(() => parsePairingCode("123456789012")).toThrow(/11 or 21 digits/);
+ });
+
+ it("rejects codes with an invalid checksum", () => {
+ const corrupted = validCode.slice(0, -1) + ((Number(validCode.at(-1)) + 1) % 10).toString();
+ expect(() => parsePairingCode(corrupted)).toThrow();
+ });
+});
diff --git a/test/config.test.ts b/test/config.test.ts
new file mode 100644
index 0000000..1caecd8
--- /dev/null
+++ b/test/config.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from "vitest";
+import { ConfigError, parseNodeId, validateConfig } from "../src/config.js";
+
+const minimal = { storagePath: "/var/lib/mattertimesync" };
+
+describe("validateConfig", () => {
+ it("accepts a minimal configuration and applies defaults", () => {
+ const config = validateConfig(minimal);
+ expect(config.storagePath).toBe(minimal.storagePath);
+ expect(config.timezone).toBe("America/Chicago");
+ expect(config.logLevel).toBe("info");
+ });
+
+ it("accepts a full configuration", () => {
+ const config = validateConfig({
+ ...minimal,
+ timezone: "Europe/Berlin",
+ logLevel: "debug",
+ });
+ expect(config.timezone).toBe("Europe/Berlin");
+ expect(config.logLevel).toBe("debug");
+ });
+
+ it("rejects non-object documents", () => {
+ expect(() => validateConfig([])).toThrow(ConfigError);
+ expect(() => validateConfig("x")).toThrow(ConfigError);
+ expect(() => validateConfig(null)).toThrow(ConfigError);
+ });
+
+ it("requires storagePath", () => {
+ expect(() => validateConfig({})).toThrow(/storagePath/);
+ expect(() => validateConfig({ storagePath: "" })).toThrow(/storagePath/);
+ });
+
+ it("rejects unknown fields", () => {
+ expect(() => validateConfig({ ...minimal, unexpected: 1 })).toThrow(/unknown field "unexpected"/);
+ // Fields from abandoned designs are rejected, not ignored.
+ expect(() => validateConfig({ ...minimal, syncIntervalHours: 24 })).toThrow(/unknown field/);
+ expect(() => validateConfig({ ...minimal, syncOnReconnect: true })).toThrow(/unknown field/);
+ expect(() => validateConfig({ ...minimal, nodeId: "1" })).toThrow(/unknown field/);
+ });
+
+ it("rejects invalid time zones", () => {
+ expect(() => validateConfig({ ...minimal, timezone: "Central Time" })).toThrow(/timezone/);
+ expect(() => validateConfig({ ...minimal, timezone: "" })).toThrow(/timezone/);
+ expect(() => validateConfig({ ...minimal, timezone: 5 })).toThrow(/timezone/);
+ });
+
+ it("rejects invalid log levels", () => {
+ expect(() => validateConfig({ ...minimal, logLevel: "verbose" })).toThrow(/logLevel/);
+ });
+});
+
+describe("parseNodeId", () => {
+ it("parses decimal strings without precision loss", () => {
+ expect(parseNodeId("1")).toBe(1n);
+ expect(parseNodeId("9007199254740993")).toBe(9007199254740993n);
+ expect(parseNodeId("18446744073709551615")).toBe(18446744073709551615n);
+ });
+
+ it("accepts small safe integers", () => {
+ expect(parseNodeId(42)).toBe(42n);
+ });
+
+ it("rejects unsafe or invalid values", () => {
+ expect(() => parseNodeId("18446744073709551616")).toThrow(ConfigError); // 2^64
+ expect(() => parseNodeId("-1")).toThrow(ConfigError);
+ expect(() => parseNodeId(1.5)).toThrow(ConfigError);
+ expect(() => parseNodeId("0x10")).toThrow(ConfigError);
+ expect(() => parseNodeId(Number.MAX_SAFE_INTEGER + 2)).toThrow(ConfigError);
+ expect(() => parseNodeId(null)).toThrow(ConfigError);
+ });
+});
diff --git a/test/fabrics.test.ts b/test/fabrics.test.ts
new file mode 100644
index 0000000..b3cd18e
--- /dev/null
+++ b/test/fabrics.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, it } from "vitest";
+import { findLikelyStrays, formatFabricTable, type FabricTable } from "../src/fabrics.js";
+
+const table: FabricTable = {
+ supportedFabrics: 5,
+ commissionedFabrics: 4,
+ entries: [
+ { fabricIndex: 1, fabricId: 100n, nodeId: 10n, vendorId: 0x1349, label: "Apple Home", isOurs: false },
+ { fabricIndex: 2, fabricId: 200n, nodeId: 20n, vendorId: 0xfff1, label: "mattertimesync", isOurs: true },
+ { fabricIndex: 3, fabricId: 300n, nodeId: 30n, vendorId: 0xfff1, label: "mattertimesync", isOurs: false },
+ { fabricIndex: 4, fabricId: 400n, nodeId: 40n, vendorId: 0xfff1, label: "mattertimesync", isOurs: false },
+ ],
+};
+
+describe("findLikelyStrays", () => {
+ it("flags entries with our label that are not our identity", () => {
+ expect(findLikelyStrays(table).map(entry => entry.fabricIndex)).toEqual([3, 4]);
+ });
+
+ it("never flags other ecosystems' entries", () => {
+ const strays = findLikelyStrays(table);
+ expect(strays.some(entry => entry.label === "Apple Home")).toBe(false);
+ });
+
+ it("flags nothing when our identity is not in the table", () => {
+ const withoutOurs: FabricTable = {
+ ...table,
+ entries: table.entries.map(entry => ({ ...entry, isOurs: false })),
+ };
+ expect(findLikelyStrays(withoutOurs)).toEqual([]);
+ });
+});
+
+describe("formatFabricTable", () => {
+ const output = formatFabricTable(1n, table);
+
+ it("shows slot usage and marks our entry", () => {
+ expect(output).toContain("4 of 5 slots used");
+ expect(output).toContain("[this controller]");
+ });
+
+ it("names known vendors and lists stale indices", () => {
+ expect(output).toContain("(Apple)");
+ expect(output).toContain("index 3, index 4");
+ });
+});
diff --git a/test/state.test.ts b/test/state.test.ts
new file mode 100644
index 0000000..a80fcfd
--- /dev/null
+++ b/test/state.test.ts
@@ -0,0 +1,103 @@
+import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import {
+ EMPTY_NODE_STATE,
+ nodeState,
+ readServiceState,
+ removeNodeState,
+ serviceStatePath,
+ updateNodeState,
+ writeServiceState,
+} from "../src/state.js";
+
+let dir: string;
+
+beforeEach(() => {
+ dir = mkdtempSync(join(tmpdir(), "mattertimesync-state-"));
+});
+
+afterEach(() => {
+ rmSync(dir, { recursive: true, force: true });
+});
+
+describe("service state", () => {
+ it("returns empty state when the file does not exist", () => {
+ expect(readServiceState(join(dir, "missing.json"))).toEqual({ nodes: {} });
+ });
+
+ it("round-trips per-node state through disk", () => {
+ const path = serviceStatePath(dir);
+ const state = {
+ nodes: {
+ "12345678901234567890": {
+ lastSuccessfulConnection: "2026-07-26T20:00:00Z",
+ lastSuccessfulSync: "2026-07-26T20:00:02Z",
+ lastAttemptedSync: "2026-07-26T20:00:01Z",
+ lastError: null,
+ },
+ "2": { ...EMPTY_NODE_STATE, lastError: "unreachable" },
+ },
+ };
+ writeServiceState(path, state);
+ expect(readServiceState(path)).toEqual(state);
+ });
+
+ it("leaves no temporary file behind after writing", () => {
+ const path = serviceStatePath(dir);
+ writeServiceState(path, { nodes: {} });
+ expect(readdirSync(dir)).toEqual(["service-state.json"]);
+ });
+
+ it("returns empty state for a corrupt file instead of throwing", () => {
+ const path = join(dir, "corrupt.json");
+ writeFileSync(path, "{ this is not json");
+ expect(readServiceState(path)).toEqual({ nodes: {} });
+ });
+
+ it("drops malformed node keys and wrongly typed fields", () => {
+ const path = join(dir, "typed.json");
+ writeFileSync(
+ path,
+ JSON.stringify({
+ nodes: {
+ "not-a-node-id": { lastError: "x" },
+ "7": { lastError: ["array"], lastSuccessfulSync: "2026-07-26T20:00:02Z" },
+ },
+ }),
+ );
+ expect(readServiceState(path)).toEqual({
+ nodes: { "7": { ...EMPTY_NODE_STATE, lastSuccessfulSync: "2026-07-26T20:00:02Z" } },
+ });
+ });
+
+ it("merges patches into one node without touching others", () => {
+ const path = serviceStatePath(dir);
+ updateNodeState(path, 1n, { lastError: "boom" });
+ updateNodeState(path, 2n, { lastSuccessfulSync: "2026-07-26T20:00:02Z" });
+ updateNodeState(path, 1n, { lastAttemptedSync: "2026-07-26T20:00:01Z" });
+
+ const state = readServiceState(path);
+ expect(nodeState(state, 1n)).toEqual({
+ ...EMPTY_NODE_STATE,
+ lastError: "boom",
+ lastAttemptedSync: "2026-07-26T20:00:01Z",
+ });
+ expect(nodeState(state, 2n)).toEqual({
+ ...EMPTY_NODE_STATE,
+ lastSuccessfulSync: "2026-07-26T20:00:02Z",
+ });
+ });
+
+ it("removes a node's entry", () => {
+ const path = serviceStatePath(dir);
+ updateNodeState(path, 1n, { lastError: "boom" });
+ updateNodeState(path, 2n, {});
+ removeNodeState(path, 1n);
+
+ const state = readServiceState(path);
+ expect(Object.keys(state.nodes)).toEqual(["2"]);
+ expect(nodeState(state, 1n)).toEqual(EMPTY_NODE_STATE);
+ });
+});
diff --git a/test/sync.test.ts b/test/sync.test.ts
new file mode 100644
index 0000000..0bc5dc9
--- /dev/null
+++ b/test/sync.test.ts
@@ -0,0 +1,111 @@
+import { describe, expect, it } from "vitest";
+import {
+ MATTER_EPOCH_UNIX_SECONDS,
+ compareDeviceClock,
+ currentMatterUtcMicroseconds,
+ formatDurationMicros,
+ formatMatterMicros,
+ matterMicrosToUnixMillis,
+ unixMillisToMatterMicros,
+} from "../src/sync.js";
+
+describe("Matter epoch conversion", () => {
+ it("maps 2000-01-01T00:00:00Z to 0 microseconds", () => {
+ const unixMs = BigInt(Date.parse("2000-01-01T00:00:00Z"));
+ expect(unixMillisToMatterMicros(unixMs)).toBe(0n);
+ });
+
+ it("maps 2000-01-01T00:00:01Z to 1,000,000 microseconds", () => {
+ const unixMs = BigInt(Date.parse("2000-01-01T00:00:01Z"));
+ expect(unixMillisToMatterMicros(unixMs)).toBe(1_000_000n);
+ });
+
+ it("keeps the Unix epoch conversion exact", () => {
+ expect(unixMillisToMatterMicros(0n)).toBe(-MATTER_EPOCH_UNIX_SECONDS * 1_000_000n);
+ expect(unixMillisToMatterMicros(0n)).toBe(-946_684_800_000_000n);
+ });
+
+ it("keeps current dates exact through bigint round-trips", () => {
+ const unixMs = BigInt(Date.parse("2026-07-26T20:00:00.123Z"));
+ const micros = unixMillisToMatterMicros(unixMs);
+ expect(micros).toBe((unixMs - 946_684_800_000n) * 1_000n);
+ expect(matterMicrosToUnixMillis(micros)).toBe(unixMs);
+ });
+
+ it("stays exact beyond JavaScript number precision", () => {
+ // Microsecond timestamps pass Number.MAX_SAFE_INTEGER around year 2285;
+ // conversion must not degrade there, proving no number arithmetic occurs.
+ const unixMs = BigInt(Date.parse("2300-01-01T00:00:00.001Z"));
+ const micros = unixMillisToMatterMicros(unixMs);
+ expect(micros > BigInt(Number.MAX_SAFE_INTEGER)).toBe(true);
+ expect(matterMicrosToUnixMillis(micros)).toBe(unixMs);
+ });
+
+ it("computes the current time via the injected clock", () => {
+ const nowMs = Date.parse("2026-07-26T20:00:00Z");
+ const micros = currentMatterUtcMicroseconds(() => nowMs);
+ expect(micros).toBe(unixMillisToMatterMicros(BigInt(nowMs)));
+ });
+
+ it("formats Matter microseconds as an ISO instant", () => {
+ expect(formatMatterMicros(0n)).toBe("2000-01-01T00:00:00.000Z");
+ expect(formatMatterMicros(1_000_000n)).toBe("2000-01-01T00:00:01.000Z");
+ });
+});
+
+describe("formatDurationMicros", () => {
+ it("scales units with magnitude", () => {
+ expect(formatDurationMicros(0n)).toBe("0us");
+ expect(formatDurationMicros(999n)).toBe("999us");
+ expect(formatDurationMicros(412_000n)).toBe("412ms");
+ expect(formatDurationMicros(3_200_000n)).toBe("3.2s");
+ expect(formatDurationMicros(59_000_000n)).toBe("59s");
+ expect(formatDurationMicros(125_000_000n)).toBe("2m 5s");
+ expect(formatDurationMicros(3_840_000_000n)).toBe("1h 4m");
+ // 90,061 seconds = 1d 1h 1m 1s; seconds are dropped once days appear.
+ expect(formatDurationMicros(90_061_000_000n)).toBe("1d 1h 1m");
+ });
+
+ it("rejects negative durations", () => {
+ expect(() => formatDurationMicros(-1n)).toThrow();
+ });
+});
+
+describe("compareDeviceClock", () => {
+ const host = unixMillisToMatterMicros(BigInt(Date.parse("2026-07-26T20:00:00.000Z")));
+
+ it("reports an unset device clock", () => {
+ const comparison = compareDeviceClock(null, host);
+ expect(comparison.deviceTime).toBeNull();
+ expect(comparison.deltaMicroseconds).toBeNull();
+ expect(comparison.hostTime).toBe("2026-07-26T20:00:00.000Z");
+ expect(comparison.description).toBe("device clock was unset");
+ });
+
+ it("reports a device clock behind the host", () => {
+ const comparison = compareDeviceClock(host - 83_000_000n, host);
+ expect(comparison.deviceTime).toBe("2026-07-26T19:58:37.000Z");
+ expect(comparison.deltaMicroseconds).toBe(-83_000_000n);
+ expect(comparison.description).toBe("device clock was 1m 23s behind");
+ });
+
+ it("reports a device clock ahead of the host", () => {
+ const comparison = compareDeviceClock(host + 5_500_000n, host);
+ expect(comparison.deltaMicroseconds).toBe(5_500_000n);
+ expect(comparison.description).toBe("device clock was 5.5s ahead");
+ });
+
+ it("treats sub-second deltas as in sync but still reports them", () => {
+ const comparison = compareDeviceClock(host - 412_000n, host);
+ expect(comparison.description).toBe("device clock was within 1s of host time (412ms behind)");
+ });
+
+ it("handles a device clock wildly in the future (wrong epoch bug)", () => {
+ // A device set with Unix-epoch seconds where Matter-epoch was expected
+ // shows up decades ahead; the delta must stay exact.
+ const wrong = host + 946_684_800n * 1_000_000n;
+ const comparison = compareDeviceClock(wrong, host);
+ expect(comparison.deltaMicroseconds).toBe(946_684_800_000_000n);
+ expect(comparison.description).toContain("ahead");
+ });
+});
diff --git a/test/timezone.test.ts b/test/timezone.test.ts
new file mode 100644
index 0000000..f6c2230
--- /dev/null
+++ b/test/timezone.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from "vitest";
+import { formatUtcOffset, isValidTimeZone, nextOffsetTransition, utcOffsetSeconds } from "../src/timezone.js";
+
+describe("isValidTimeZone", () => {
+ it("accepts IANA names", () => {
+ expect(isValidTimeZone("America/Chicago")).toBe(true);
+ expect(isValidTimeZone("Europe/Berlin")).toBe(true);
+ expect(isValidTimeZone("UTC")).toBe(true);
+ });
+
+ it("rejects invalid names", () => {
+ expect(isValidTimeZone("Central Time")).toBe(false);
+ expect(isValidTimeZone("")).toBe(false);
+ expect(isValidTimeZone("America/Springfield")).toBe(false);
+ });
+});
+
+describe("utcOffsetSeconds", () => {
+ it("returns CST offset in January", () => {
+ expect(utcOffsetSeconds("America/Chicago", new Date("2026-01-15T12:00:00Z"))).toBe(-6 * 3600);
+ });
+
+ it("returns CDT offset in July", () => {
+ expect(utcOffsetSeconds("America/Chicago", new Date("2026-07-15T12:00:00Z"))).toBe(-5 * 3600);
+ });
+
+ it("returns 0 for UTC", () => {
+ expect(utcOffsetSeconds("UTC", new Date("2026-07-15T12:00:00Z"))).toBe(0);
+ });
+
+ it("handles half-hour offsets", () => {
+ expect(utcOffsetSeconds("Asia/Kolkata", new Date("2026-07-15T12:00:00Z"))).toBe(5.5 * 3600);
+ });
+});
+
+describe("formatUtcOffset", () => {
+ it("formats negative, positive, and zero offsets", () => {
+ expect(formatUtcOffset(-6 * 3600)).toBe("UTC-06:00");
+ expect(formatUtcOffset(5.5 * 3600)).toBe("UTC+05:30");
+ expect(formatUtcOffset(0)).toBe("UTC+00:00");
+ });
+});
+
+describe("nextOffsetTransition", () => {
+ it("finds the exact spring-forward instant for Chicago", () => {
+ const transition = nextOffsetTransition("America/Chicago", new Date("2026-01-15T00:00:00Z"));
+ expect(transition).not.toBeNull();
+ // 2026-03-08 02:00 CST (UTC-6) -> 03:00 CDT (UTC-5): 08:00:00 UTC.
+ expect(transition!.at.toISOString()).toBe("2026-03-08T08:00:00.000Z");
+ expect(transition!.offsetBeforeSeconds).toBe(-6 * 3600);
+ expect(transition!.offsetAfterSeconds).toBe(-5 * 3600);
+ });
+
+ it("finds the exact fall-back instant for Chicago", () => {
+ const transition = nextOffsetTransition("America/Chicago", new Date("2026-07-15T00:00:00Z"));
+ expect(transition).not.toBeNull();
+ // 2026-11-01 02:00 CDT (UTC-5) -> 01:00 CST (UTC-6): 07:00:00 UTC.
+ expect(transition!.at.toISOString()).toBe("2026-11-01T07:00:00.000Z");
+ expect(transition!.offsetBeforeSeconds).toBe(-5 * 3600);
+ expect(transition!.offsetAfterSeconds).toBe(-6 * 3600);
+ });
+
+ it("returns null for fixed-offset zones", () => {
+ expect(nextOffsetTransition("UTC", new Date("2026-01-15T00:00:00Z"))).toBeNull();
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..c68f719
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "types": ["node"],
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "declaration": false
+ },
+ "include": ["src/**/*.ts"]
+}