diff --git a/.github/workflows/consumer-nlos-ci.yml b/.github/workflows/consumer-nlos-ci.yml new file mode 100644 index 00000000..ae2c3b03 --- /dev/null +++ b/.github/workflows/consumer-nlos-ci.yml @@ -0,0 +1,180 @@ +name: Consumer NLOS + +on: + push: + branches: [main, 'feat/**', 'feature/**'] + paths: + - 'v2/crates/ruview-nlos/**' + - 'ui/ios-nlos/**' + - 'ui/mobile/**' + - 'harness/ruview/**' + - 'docs/adr/ADR-32[8-9]*' + - 'docs/adr/ADR-33[0-1]*' + - 'docs/research/consumer-nlos-acceptance-protocol.md' + - 'docs/schemas/ruview-nlos-*.schema.json' + - 'docs/security/consumer-nlos-threat-model.md' + - '.github/workflows/consumer-nlos-ci.yml' + - 'v2/Cargo.toml' + - 'v2/Cargo.lock' + pull_request: + branches: [main] + paths: + - 'v2/crates/ruview-nlos/**' + - 'ui/ios-nlos/**' + - 'ui/mobile/**' + - 'harness/ruview/**' + - 'docs/**consumer-nlos*' + - '.github/workflows/consumer-nlos-ci.yml' + - 'v2/Cargo.toml' + - 'v2/Cargo.lock' + workflow_dispatch: + +permissions: + contents: read + +jobs: + rust-core: + name: Rust core, security, and performance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + submodules: recursive + + - name: Install Rust + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + components: rustfmt, clippy + + - name: Cache Cargo + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2 + + - name: Test dependency-free core + working-directory: v2 + run: cargo test -p ruview-nlos --no-default-features + + - name: Formatting and lint + working-directory: v2 + run: | + cargo fmt --package ruview-nlos -- --check + cargo clippy -p ruview-nlos --all-features --all-targets -- -D warnings + + - name: Test server and hardware adapters + working-directory: v2 + run: cargo test -p ruview-nlos --all-features + + - name: Check all targets + working-directory: v2 + run: cargo check -p ruview-nlos --all-features --all-targets + + - name: Audit Rust lockfile + working-directory: v2 + run: | + cargo install cargo-audit --version 0.22.2 --locked + cargo audit + + - name: Measure synthetic architecture gate + working-directory: v2 + run: | + cargo run -p ruview-nlos --release -- benchmark --frames 300 --particles 1000 > /tmp/nlos-benchmark.json + python - <<'PY' + import json + from pathlib import Path + + report = json.loads(Path('/tmp/nlos-benchmark.json').read_text()) + assert report['evidence'] == 'SYNTHETIC_L0' + assert report['hardwareReproductionGatePassed'] is False + assert report['throughputFps'] >= 30.0, report + assert report['lostTrackReductionPercent'] >= 25.0, report + print(json.dumps(report, indent=2, sort_keys=True)) + PY + + web-ios: + name: Expo web iOS client + runs-on: ubuntu-latest + defaults: + run: + working-directory: ui/mobile + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + cache: npm + cache-dependency-path: ui/mobile/package-lock.json + + - run: npm ci --ignore-scripts + - run: npm test -- --runInBand + - run: npx tsc --noEmit + - run: npm run lint + - name: Export browser bundle + run: npx expo export --platform web + - name: Export Expo iOS bundle + run: npx expo export --platform ios --output-dir /tmp/ruview-expo-ios + - name: Audit web dependency boundary + run: | + npm audit --json > /tmp/mobile-audit.json || true + node <<'JS' + const report = require('/tmp/mobile-audit.json'); + const severe = Object.entries(report.vulnerabilities ?? {}) + .filter(([, finding]) => ['high', 'critical'].includes(finding.severity)); + if (severe.length) { + console.error(JSON.stringify(severe, null, 2)); + process.exit(1); + } + console.log(JSON.stringify(report.metadata?.vulnerabilities ?? {}, null, 2)); + JS + + native-ios: + name: Native Swift and iOS Simulator build + runs-on: macos-15 + defaults: + run: + working-directory: ui/ios-nlos + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Swift protocol and security tests + run: swift test + + - name: Unsigned iOS Simulator build + run: >- + xcodebuild + -project RuViewNLOS.xcodeproj + -scheme RuViewNLOS + -sdk iphonesimulator + -destination 'generic/platform=iOS Simulator' + CODE_SIGNING_ALLOWED=NO + build + + metaharness: + name: Advisory MetaHarness + runs-on: ubuntu-latest + defaults: + run: + working-directory: harness/ruview + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Set up Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + cache: npm + cache-dependency-path: harness/ruview/package-lock.json + + - run: npm ci --ignore-scripts + - run: npm test + - run: npm run test:security + - run: npm run brain:verify + - run: npm run flywheel:verify + - run: npm run manifest:verify + - run: npm pack --dry-run diff --git a/.github/workflows/npm-packages.yml b/.github/workflows/npm-packages.yml index 71c54550..af4253ff 100644 --- a/.github/workflows/npm-packages.yml +++ b/.github/workflows/npm-packages.yml @@ -40,9 +40,10 @@ jobs: - dir: harness/ruview build: false publishable: true - # ADR-283/325: brain + local hosts + replay assets + guarded Spaces OAuth adapter; - # still runtime-dependency-free. 160 KiB is the reviewed hard ceiling. - unpacked_budget: 163840 + # ADR-283/325/331: brain + local hosts + replay assets + guarded + # Spaces OAuth + consumer-NLOS verifier; still runtime-dependency-free. + # 220 KiB is the reviewed hard ceiling after the bounded NLOS addition. + unpacked_budget: 225280 - dir: harness/homecore build: false publishable: true diff --git a/.github/workflows/ruview-npm-release.yml b/.github/workflows/ruview-npm-release.yml index 2d2c977c..9a9642be 100644 --- a/.github/workflows/ruview-npm-release.yml +++ b/.github/workflows/ruview-npm-release.yml @@ -104,8 +104,9 @@ jobs: run: | set -euo pipefail case "${{ inputs.package }}" in - # ADR-283/325: brain + hosts + replay + guarded Spaces OAuth; no runtime deps. - harness/ruview) export UNPACKED_BUDGET=163840 ;; + # ADR-283/325/331: brain + hosts + replay + guarded Spaces OAuth + + # bounded consumer-NLOS verifier; no runtime dependencies. + harness/ruview) export UNPACKED_BUDGET=225280 ;; # ADR-285: CLI + MCP + reviewed brain + WASM-kernel adapter. harness/homecore) export UNPACKED_BUDGET=180000 ;; # ADR-264 O2: map-free tarball (was 188 kB with maps). diff --git a/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md b/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md new file mode 100644 index 00000000..20d94c89 --- /dev/null +++ b/docs/adr/ADR-328-consumer-nlos-raw-transient-reproduction-pipeline.md @@ -0,0 +1,251 @@ +# ADR-328: Consumer NLOS raw-transient pipeline and upstream reproduction boundary + +| Field | Decision | +|---|---| +| **Status** | Accepted for staged implementation; software contracts may be validated in CI, live-hardware reproduction remains pending until a witness capture passes the protocol | +| **Date** | 2026-08-22 | +| **Owners** | RuView Labs maintainers and sensing research reviewers | +| **Scope** | Commodity optical transient acquisition, calibration, normalization, provenance, upstream reproduction | +| **Extends** | ADR-295, ADR-303, ADR-305, ADR-319, ADR-320 | +| **Related** | ADR-329, ADR-330, ADR-331 | +| **Primary implementation** | `v2/crates/ruview-nlos`, upstream `sidsoma/consumer-nlos`, `harness/ruview` advisory verification | + +## Context + +Somasundaram et al. introduce motion-induced aperture sampling (MAS) for +consumer time-of-flight LiDAR. Their measurement is not an ordinary depth map. +Each sensor zone records light intensity over time. The strong direct relay-wall +return is followed by much weaker multipath returns whose path lengths constrain +hidden geometry. Multiple frames supply redundant and spatially diverse samples +that improve signal-to-noise ratio and synthesize a larger virtual aperture. + +The [Nature paper](https://doi.org/10.1038/s41586-026-10502-x), +[author manuscript](https://arxiv.org/html/2605.17865v1), and +[MIT project page](https://cornar.media.mit.edu/) report 3D reconstruction, +single and multi-object tracking, camera localization, and real-time tracking at +30 Hz. These are upstream `CLAIMED` results until RuView reproduces them with a +named live capture. They are not evidence that Apple exposes the required +measurement to an ordinary iOS application. + +The upstream [consumer-nlos implementation](https://github.com/sidsoma/consumer-nlos) +targets ST's P-NUCLEO-53L8A1 research path and captures per-zone histograms. ST +documents the [VL53L8CH](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html) +compact-normalized-histogram interface and configuration-dependent zone/bin/rate +limits. The exact silicon, expansion board, firmware/API and transient format +must be read from and bound to each capture rather than inferred from the kit +name. The +[P-NUCLEO-53L8A1](https://www.st.com/en/evaluation-tools/p-nucleo-53l8a1.html) +combines the expansion board with an STM32 Nucleo host. + +RuView needs an explicit boundary because flattening the input to point clouds, +`Observation.value`, ARKit depth, or CSI destroys the delayed transient that the +inverse problem needs. A successful build or replay also cannot validate the +photon path, relay geometry, ambient-light behavior, or target reflectivity. + +## Decision + +### 1. Reproduce upstream before modifying its inference state + +Phase 1 uses a pinned commit of the upstream implementation and a documented, +upstream-compatible ST assembly with verified raw or compact-normalized +histogram access. The current reference adapter targets VL53L8CH framing, but +the acceptance record stores the observed board/silicon identity and never +promotes a model name inferred from packaging. RuView records the exact upstream +commit, firmware/API digest, scoped enrollment/certificate reference, configuration, calibration +digest, and capture-manifest digest. Upstream code runs as an isolated research +sidecar or separately reviewed tool; it is not silently vendored into the +production Rust dependency graph. + +The first geometry follows the upstream plug-and-play arrangement: + +1. a planar relay surface fills the sensor field of view; +2. an opaque occluder prevents direct line of sight to the target; +3. a known rigid retroreflective target supplies the initial high-SNR case; +4. an independent ground-truth system measures the hidden trajectory; and +5. an empty-scene background is captured before each experimental block. + +Diffuse rigid objects and people are later strata. They cannot be pooled into +the retroreflective acceptance result. The manuscript notes weaker diffuse +returns and an approximately fourth-power distance falloff in its diffuse model; +the project therefore reports performance by target material and range. + +### 2. Preserve a first-class transient frame + +`v2/crates/ruview-nlos` owns the initial versioned transient scaffold. The table +below is the capture-manifest/next-contract requirement for live promotion, not +a claim that every field already exists in the current v1 Rust/track schema. +The current scaffold implements bounded histogram/provenance/calibration pieces; +missing clock-domain, certificate, world-frame, configuration, and capture +bindings must land in a reviewed schema before L2. The eventual wire/storage +representation retains, at minimum: + +| Field group | Required content | Rejection rule | +|---|---|---| +| Identity | schema version, sensor ID or certificate reference, session ID, sequence | unknown schema, unauthenticated identity, duplicate or regressing sequence | +| Time | sensor monotonic timestamp, host receive timestamp, clock domain and uncertainty | future, stale, non-finite, or unbounded clock error | +| Histogram | zone layout, temporal bin count and width, signed/unsigned count encoding, ambient estimate | zero/oversized dimensions, non-finite values, count overflow | +| Geometry | per-zone ray or relay-wall point, sensor intrinsics, world transform, coordinate-frame ID | non-invertible transform, unit mismatch, incompatible frame | +| Calibration | direct-return peak, mask, background reference, calibration and configuration digests | absent, expired, mismatched, or out-of-distribution calibration | +| Provenance | source `LIVE_HARDWARE`/`REPLAY`/`SYNTHETIC`, firmware and capture digests, evidence level | unknown never becomes live; replay and synthetic are visibly distinct | + +The HAL may wrap the frame as a modality-specific payload, but it must not +reduce the histogram to a scalar before NLOS preprocessing. Raw transient input +has its own size/rate limits and parser fuzz surface. + +### 3. Deterministic normalization stages + +The reference normalization pipeline is ordered and individually testable: + +1. verify identity, schema, bounds, sequence, timestamps, and calibration; +2. subtract a compatible empty-scene background; +3. locate and mask the strong one-bounce relay-surface return per zone; +4. align each zone's direct peak to the agreed temporal origin; +5. reject saturated, underexposed, or calibration-incompatible zones; +6. transform time/depth into the light-cone coordinate used by MAS; and +7. emit a normalized transient plus quality flags, never an unconditional track. + +Calibration is an explicit state machine: `UNAVAILABLE`, `CAPTURING`, `VALID`, +`DEGRADED`, `EXPIRED`, or `REJECTED`. Only `VALID` calibration can produce a +live optical likelihood. Loss of calibration produces `unknown`, not a cached +or synthetic fallback. + +### 4. Separate acquisition evidence from inference evidence + +The capture manifest is append-only and content-addressed. It binds the raw +stream, exclusions, firmware, configuration, calibration, clock synchronization, +target/relay-surface stratum, and ground-truth source. Track records reference +the capture and model digests. RuVector or a particle filter may improve +temporal inference, but neither can upgrade acquisition provenance or fabricate +unobserved photons. + +Raw captures are local research data by default and are not committed to Git. +Only schemas, small non-person fixtures, checksums, and aggregate metrics may be +reviewed in the repository under the data policy. + +## Performance decision + +The first live reproduction targets end-to-end track updates at **at least 27 +Hz**, a preregistered operational definition of “roughly 30 fps.” The rate is +measured from accepted sensor frame through emitted track, not the configured +sensor clock. Dropped, duplicated, replayed, or late frames are not counted. +The frozen zone/bin/integration configuration must itself be documented and +demonstrated capable of this rate; ST configurations documented at 25 Hz cannot +pass merely because another configuration has a 30 Hz maximum. + +Implementation budgets are: + +| Stage | Budget and behavior | +|---|---| +| Parser and provenance | bounded allocation; reject before copying oversized dimensions | +| Normalization | one pass over zones × bins; reusable buffers; no unbounded capture queue | +| Tracking | bounded particle count and search volume; overload drops stale work rather than growing latency | +| End-to-end | report p50/p95 latency, update rate, frame loss, CPU and memory with named reproducer | + +The paper describes 1,000 particles and a 5 cm proximity prior at 30 Hz. Those +are starting parameters, not RuView guarantees. Optimization must preserve a +golden likelihood/track tolerance and may not hide quality loss behind pooled +throughput. + +## Security and privacy + +1. USB/serial is the initial acquisition transport. A future network bridge + requires authenticated sensor identity, encryption, replay protection, + explicit bind configuration, and a separate threat review. No new + unauthenticated UDP listener is introduced. +2. Firmware and upstream code are supply-chain inputs. Pin commits and toolchain + versions, review licenses, scan dependencies, and verify build/capture + digests. Flashing remains an explicit, confirmed hardware mutation. +3. Malformed frames, NaN/Inf values, integer products, timestamp wrap, + decompression bombs, calibration substitution, and coordinate transforms are + fail-closed parser cases. +4. NLOS presence and trajectory data are sensitive even without imagery. The + approved operator/controller and every required participant receive purpose/ + space/time notice and consent controls, pause/withdrawal and a persistent + indicator; raw retention is bounded and session track IDs never claim identity. +5. Safety-critical actuation is outside this ADR. A hypothesis never directly + drives a lock, vehicle, medical device, or emergency decision. + +The full attacker/asset analysis is in +`docs/security/consumer-nlos-threat-model.md`. + +## Alternatives considered + +### Feed ARKit scene depth directly into the MIT algorithm + +Rejected. Apple's documented scene-depth surface is processed distance data, +not the per-zone photon-arrival histogram required by the image formation +model. ARKit remains useful for pose and visible geometry under ADR-330. + +### Treat CSI as a synthetic optical transient + +Rejected. CSI and optical time-of-flight measure different physical channels. +CSI can contribute an independently calibrated likelihood under ADR-329; it +cannot replace the optical measurement or supervise itself. + +### Port upstream code to Rust before reproducing it + +Rejected for Phase 1. Simultaneously changing hardware, forward model, state +estimator, and language makes failures uninterpretable. The Rust port follows a +pinned upstream baseline and golden captures. + +### Vendor all upstream firmware, Python, and data into RuView + +Rejected. It increases supply-chain, license, binary-size, and data-governance +risk. The reproduction boundary uses pins, manifests, adapters, and small legal +fixtures instead. + +## Consequences + +### Positive + +1. RuView gains an honest optical-transient modality without corrupting CSI or + scalar HAL semantics. +2. Upstream reproduction and RuView extensions remain experimentally separable. +3. Provenance, calibration, and failure states survive into every downstream + hypothesis. +4. The same contract can later support another histogram-capable ToF sensor. + +### Costs and limitations + +1. Initial use requires an external ST board and ground-truth rig. +2. Raw histograms and CSI increase sensitive data volume and governance burden. +3. Calibration, reflectivity, relay geometry, ambient light, and motion can + dominate algorithm changes. +4. This decision does not establish through-wall optical sensing, unrestricted + human reconstruction, identity, or production safety. + +## Rollout and rollback + +| Phase | Enable condition | Rollback trigger | Rollback action | +|---|---|---|---| +| R0 fixtures | schema/parser/property tests pass | parser panic, unbounded allocation, provenance ambiguity | disable crate feature and retain fixtures only | +| R1 upstream live reproduction | approved protocol, verified histogram interface, external ground truth, and `MEASURED` >=27 Hz accepted-update witness | calibration drift, direct line of sight, saturation, provenance gap | invalidate capture; return to controlled geometry | +| R2 Rust shadow | R1 remains valid and golden-capture agreement is within frozen tolerance | likelihood/track divergence or latency regression | keep upstream sidecar authoritative; disable Rust output | +| R3 candidate live | independent witness plus ADR-331 confidence/security/privacy/guardrail gate | research endpoint fails or privacy controls regress | emit `unknown`; withdraw capability certificate | + +Rollback never relabels a failed live capture as a passing replay. Existing CSI, +RuVector, WorldGraph, and other RuView runtime paths remain independently usable. + +## Objective acceptance mapping + +| ID | Requirement | Evidence | +|---|---|---| +| NLOS-328-01 | Preserve bounded per-zone timing histograms and calibration provenance | Rust round-trip, boundary, fuzz/property, golden-vector tests | +| NLOS-328-02 | Reject stale, duplicate, oversized, unauthenticated, non-finite, and calibration-mismatched input | Negative parser and state-machine tests | +| NLOS-328-03 | Keep `LIVE_HARDWARE`, `REPLAY`, and `SYNTHETIC` mutually exclusive | provenance transition tests and capture manifest review | +| NLOS-328-04 | Reproduce hidden-target tracking at roughly 30 fps | `MEASURED` live capture, external ground truth, >=27 accepted updates/s | +| NLOS-328-05 | Prevent synthetic-only acceptance | `ruview nlos verify --require-research-pass` rejection tests | +| NLOS-328-06 | Make upstream/Rust comparison reproducible | pinned commit, firmware/config/calibration/capture SHA-256 digests | +| NLOS-328-07 | Preserve normal RuView operation when NLOS is absent | workspace tests with crate/sensor disabled; advisory harness reports `ABSENT` | + +The authoritative experimental steps and statistical gate are in +`docs/research/consumer-nlos-acceptance-protocol.md`. + +## References + +1. Somasundaram et al., [“Imaging Hidden Objects with Consumer LiDAR via Motion Induced Sampling”](https://doi.org/10.1038/s41586-026-10502-x), Nature 653, 693–699 (2026). +2. [Author manuscript and methods](https://arxiv.org/html/2605.17865v1). +3. [MIT Consumer NLOS project](https://cornar.media.mit.edu/). +4. [Upstream implementation](https://github.com/sidsoma/consumer-nlos). +5. STMicroelectronics, [VL53L8CH product specification](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html). +6. STMicroelectronics, [P-NUCLEO-53L8A1 evaluation kit](https://www.st.com/en/evaluation-tools/p-nucleo-53l8a1.html). diff --git a/docs/adr/ADR-329-motion-induced-aperture-csi-fusion-world-model.md b/docs/adr/ADR-329-motion-induced-aperture-csi-fusion-world-model.md new file mode 100644 index 00000000..f5bf43b5 --- /dev/null +++ b/docs/adr/ADR-329-motion-induced-aperture-csi-fusion-world-model.md @@ -0,0 +1,285 @@ +# ADR-329: Motion-induced aperture inference, CSI fusion, and RuView world-state integration + +| Field | Decision | +|---|---| +| **Status** | Accepted for staged implementation; software fusion is testable with replay, retained performance claims require the live paired protocol | +| **Date** | 2026-08-22 | +| **Owners** | RuView Labs perception, RuVector, RuField, and WorldGraph maintainers | +| **Scope** | MAS forward model, temporal inference, calibrated RF/optical fusion, persistent identity-free state | +| **Depends on** | ADR-273, ADR-295, ADR-301 through ADR-305, ADR-311, ADR-319, ADR-328 | +| **Related** | ADR-330, ADR-331 | +| **Primary implementation** | `v2/crates/ruview-nlos`, `ruview.nlos.track.v1` | + +## Context + +The consumer-NLOS model converts weak, time-resolved optical multipath into a +posterior over hidden geometry or motion. Its strength is localized geometry +around a relay surface; its weaknesses include low signal-to-noise ratio, +reflectivity, range, aperture coverage, and temporal assumptions. RuView CSI +offers a different failure profile: RF can persist through walls and clutter, +but current commodity CSI generally provides coarser, environment-dependent +spatial evidence. + +These modalities should not be concatenated merely because both are +interesting. Fusion is justified only when an independently evaluated RF +likelihood improves an objective endpoint over optical NLOS alone. It is also +unsafe to let a learned temporal store or graph overwrite current sensing +quality. RuVector, RuField, and WorldGraph must preserve uncertainty, freshness, +calibration, and provenance rather than manufacture confidence. + +The [consumer-NLOS paper](https://arxiv.org/html/2605.17865v1) expresses tracking +as a sequential posterior and uses a particle filter to carry uncertainty and a +motion prior. This aligns with RuView's temporal and spatial primitives, but the +physics likelihood remains the authority for the optical observation. + +## Decision + +### 1. Use a typed latent state and explicit likelihood factors + +For each session-scoped hidden target hypothesis, the latent state is + +\[ +X_t = (p_t, v_t, \Sigma_t, q_t, a_t) +\] + +where position \(p_t\), velocity \(v_t\), covariance or particle distribution +\(\Sigma_t\), quality state \(q_t\), and bounded attributes \(a_t\) are expressed +in a versioned world coordinate frame. No identity field is inferred. + +The fused filter computes + +\[ +P(X_t \mid L_{1:t}, R_{1:t}) \propto +P(L_t \mid X_t, C_L)^{w_L} +P(R_t \mid X_t, C_R)^{w_R} +P(X_t \mid X_{t-1}), +\] + +where \(L_t\) is the normalized optical transient, \(R_t\) is independently +captured CSI evidence, and \(C_L,C_R\) are modality-specific calibration states. +The exponents are bounded reliability weights derived from current quality, +not user-facing confidence decoration. +Missing, stale, rejected or invalid modalities are omitted before factor +evaluation and contribute the multiplicative identity. They are never evaluated +as a zero likelihood raised to a zero weight (`0^0`). + +Conditional independence is an approximation. Correlated errors such as motion, +clock drift, shared ground-truth leakage, and environmental change are measured +and documented. If correlation cannot be bounded, the implementation uses a +conservative mixture, covariance intersection, or gating rule instead of +multiplying overconfident factors. + +### 2. Keep the MAS physics path inspectable + +The optical likelihood follows the upstream structure: + +1. transform the normalized transient into light-cone coordinates; +2. precompute a canonical space-time impulse response for the known target + shape or reconstruction basis; +3. propagate a bounded particle set using the frozen motion model; +4. render each particle by indexing the canonical response at current relay-wall + samples and pose; +5. score rendered and observed transients with a normalized, bounded likelihood; +6. normalize, calculate degeneracy diagnostics, and residual-resample; and +7. emit the posterior, entropy/effective-particle count, covariance, and quality. + +Optimization may vectorize or cache rendering, but every optimized kernel is +compared against a scalar reference over golden and property-generated inputs. +NaN/Inf, all-zero likelihood, underflow, particle collapse, and out-of-volume +states produce a typed degradation or `unknown` result. + +### 3. Calibrate CSI as a likelihood, not a centimetre claim + +The initial RF factor is deliberately modest. It may provide: + +1. hidden-region presence or absence likelihood; +2. coarse zone occupancy; +3. motion onset/cessation likelihood; +4. a broad position prior with measured covariance; or +5. an empty-volume vote for guarded optical background updates. + +RF weights are zero when the CSI sensor identity, room calibration, coordinate +transform, freshness, or out-of-distribution gate is invalid. CSI never sharpens +an optical posterior beyond the calibration evidence that justifies it. A model +trained with LiDAR pseudo-labels is evaluated on a held-out partition with +external ground truth; it may not be scored against its own optical teacher. + +### 4. Synchronize before fusion + +Both inputs enter a bounded temporal join keyed by tenant, workspace, site, +world-frame ID, and capture session. The join records sensor timestamp, receive +timestamp, clock uncertainty, calibration digest, and maximum pairing skew. + +Frames outside the preregistered skew are not interpolated into apparent +coherence. The system may make an optical-only or RF-only observation with the +missing modality named, but it may label output `fused` only when both factors +pass identity, freshness, calibration, and time/space alignment checks. + +### 5. Divide responsibilities across RuView primitives + +| Component | Responsibility | Explicit non-responsibility | +|---|---|---| +| MAS/particle filter | current optical likelihood and posterior update | long-term identity, authorization, hardware provenance creation | +| CSI adapter/model | current calibrated RF likelihood | optical histogram synthesis, centimetre precision without evidence | +| RuVector | embeddings for trajectory/canonical-response similarity and bounded temporal retrieval | replacing the live Bayesian update or upgrading evidence level | +| RuField | observation confidence, covariance, quality, provenance, calibration, and expiration | presenting unknown/stale data as ground truth | +| WorldGraph | session-scoped hidden-object hypothesis nodes and spatial/temporal relations | identity inference or permanent person graph by default | +| Evidence/witness layer | content digests, acceptance result, lineage and receipts | sensing, fusion, or actuation | + +`ruview.nlos.track.v1` is the current shared output contract. It carries +track/session IDs, one source classification, position/velocity, covariance, +freshness/expiry, quality state, modality contribution weights, calibration +hash, evidence level, algorithm revision, and one provenance record. It does +not carry two authenticated modality lineages, a world-frame ID, coordinate +transform digest, or capture-manifest digest. Consequently v1 rejects measured +CSI fusion and `l3_corroborated`; the current CSI path is scope-bound synthetic +L0 regression only. A future v2 contract must add those bindings before F1/F2 +measured fusion can be enabled. Consumers reject unknown schema versions and +stale tracks now; coordinate compatibility remains a future contract gate. + +### 6. Cross-modal training follows, and cannot contaminate, evaluation + +After a frozen rules/physics fusion baseline, LiDAR may supervise an RF model. +Dataset partitions are grouped by capture session, subject/target, room, sensor +configuration, and time block to prevent adjacent-frame and environment +leakage. Optical targets for RF training are probabilistic distributions with +quality masks, not hard ground truth. The test endpoint always uses independent +external ground truth and includes LiDAR-only, CSI-only, and fused arms. + +The system retains a non-learned fallback. A learned score or fusion policy must +show stratified improvement and calibration before promotion. Model absence or +error degrades to the last independently validated factor, never direct action. + +## Architecture and data flow + +```mermaid +flowchart TD + A["Transient frame + optical calibration"] --> B["MAS optical likelihood"] + C["CSI frame + RF calibration"] --> D["RF likelihood"] + B --> E["Bounded temporal Bayesian join"] + D --> E + E --> F["ruview.nlos.track.v1"] + F --> G["RuField + RuVector + WorldGraph"] +``` + +Only the join creates a fused track. RuVector and WorldGraph are downstream +state consumers, not a shortcut around missing, stale, or rejected modalities. + +## Performance decision + +1. The live tracking loop is bounded to the newest accepted frame. Backpressure + drops obsolete queued work and records loss; it does not accumulate latency. +2. The particle count, canonical volume, search volume, history length, graph + nodes per session, and temporal-join window are configuration-bounded. +3. The fusion budget is measured independently from optical tracking. Report + p50/p95 sensor-to-track latency and update rate for LiDAR-only and fused arms. +4. Optimization is retained only when numerical agreement stays within the + frozen tolerance and the paired research endpoint does not regress. +5. At least 27 accepted LiDAR-only track updates per second is required before + the fusion endpoint is interpreted. A fast fused path cannot rescue a failed + reproduction. + +## Security and privacy + +1. The join accepts only authenticated sensor/session identities and compatible + tenant/workspace/world-frame bindings. Cross-tenant or cross-session joins + are impossible by type and policy. +2. Sequence and timestamp replay defenses are per modality. Reusing an old CSI + frame to make an optical track look persistent is rejected and audited. +3. A malicious modality may inject extreme likelihoods. Inputs and weights are + bounded, posterior influence is observable, and single-modality ablations are + recorded for forensic review. +4. Track IDs are random and session-scoped. Raw optical/CSI data is local by + default; downstream stores receive bounded hypotheses and provenance unless + an approved research protocol explicitly retains raw data. +5. Long-term memory uses TTL, purpose limitation, deletion, and tenant isolation. + Similarity is not identity. The graph must not create a biometric profile. +6. No fusion output grants actuation. Governed actions require the independent + ADR-321/327 policy, approval, freshness, and receipt path. + +## Alternatives considered + +### Concatenate optical and CSI tensors into one end-to-end network + +Deferred. It obscures failure attribution, calibration, and missing-modality +behavior before a trustworthy baseline exists. A learned likelihood may be +added after the factorized paired benchmark. + +### Use CSI only as a binary veto + +Useful as an initial safety rule, but insufficient as the final design because +it discards calibrated spatial/motion information. The typed factor supports a +binary likelihood without fixing the architecture to it. + +### Store only the posterior mean + +Rejected. NLOS can be multimodal and ambiguous. Covariance, particles or a +bounded distribution summary, posterior entropy, and quality are essential for +honest downstream behavior. + +### Let RuVector replace the particle filter + +Rejected. Vector memory can retrieve similar histories or canonical responses, +but similarity alone is not the current physics likelihood and cannot enforce +frame-level calibration/provenance. + +### Promote fusion if any pooled metric improves + +Rejected. The objective endpoint, pairing, strata, confidence interval, and +non-success metrics are frozen before capture. Synthetic-only and pooled-only +gains do not pass. + +## Consequences + +### Positive + +1. Optical localization evidence and RF persistence can complement one another without + conflating their measurement physics. +2. Every downstream state retains uncertainty, freshness, modality, and lineage. +3. The factorized baseline makes ablation, failure analysis, and rollback clear. +4. Cross-modal training has a leakage-resistant target and external evaluation. + +### Costs and limitations + +1. Coordinate/clock calibration and paired ground truth add operational burden. +2. Conditional-independence violations can make naive multiplication + overconfident; conservative fusion may sacrifice apparent sharpness. +3. Current CSI may not provide enough independent spatial information to meet + the 25 percent endpoint. A negative result is acceptable and stops rollout. +4. The architecture tracks hypotheses; it does not establish identity, intent, + photographic reconstruction, or safety certification. + +## Rollout and rollback + +| Phase | Behavior | Promotion | Rollback | +|---|---|---|---| +| F0 | optical-only reference plus recorded CSI | deterministic replay and calibration tests | disable RF factor | +| F1 | shadow fusion, output not consumed | ADR-328 live LiDAR-only reproduction is `MEASURED` at >=27 Hz in its capture-manifest witness report and paired live capture is complete | discard shadow output | +| F2 | research-visible fused track with explicit evidence | ADR-328 prerequisite remains valid; `MEASURED` >=25% endpoint has adjusted confidence support plus frozen-protocol, independent-CSI, security/privacy, guardrail and witness review | return to optical-only; invalidate certificate | +| F3 | cross-modal learned likelihood in shadow | held-out external-ground-truth gain and calibration | remove model artifact; retain factorized baseline | + +Rollback is a configuration/capability-certificate change. Existing optical, +CSI, RuVector, RuField, and WorldGraph services continue independently. + +## Objective acceptance mapping + +| ID | Requirement | Evidence | +|---|---|---| +| NLOS-329-01 | Reference MAS likelihood is deterministic and bounded | scalar/golden/property tests, all-zero, omitted-factor/`0^0`, and numeric-extreme tests | +| NLOS-329-02 | Fused output requires valid identity, calibration, freshness, clocks, and coordinate frame for both factors | negative temporal/spatial join matrix plus missing/stale-factor omission tests | +| NLOS-329-03 | Missing/rejected modality cannot be mislabeled fused | modality/provenance state tests | +| NLOS-329-04 | RuVector/RuField/WorldGraph preserve uncertainty, TTL, lineage, and session scope | contract and integration tests | +| NLOS-329-05 | Fusion improves a preregistered objective endpoint | F2/ADR-331 gate under one frozen protocol over >=100 paired `LIVE_HARDWARE` sequences: `MEASURED` >=25% mean-position-error **or** lost-track-rate reduction, successful multiplicity-adjusted interval excludes zero, shared endpoint-pairing digest and full position coverage, all frozen guardrails pass, independent CSI plus privacy/security/witness reviews pass | +| NLOS-329-06 | Evaluation is leakage resistant | grouped partitions, frozen protocol, external ground truth, LiDAR/CSI/fused ablation | +| NLOS-329-07 | Optimization preserves correctness and bounded latency | reference equivalence, benchmark deltas, overload/backpressure tests | +| NLOS-329-08 | Fusion cannot actuate directly | policy/API tests proving hypothesis-only output | + +## References + +1. Somasundaram et al., [Nature article](https://doi.org/10.1038/s41586-026-10502-x). +2. [Motion-induced aperture model and particle-filter methods](https://arxiv.org/html/2605.17865v1). +3. ADR-273: Unified RF spatial world model. +4. ADR-295: Source provenance state machine. +5. ADR-301/302/303/304/305: calibration, OOD, ground truth, evidence, and authenticated sensor identity. +6. ADR-311: Real sensor fusion. +7. ADR-319: Witness chain. diff --git a/docs/adr/ADR-330-native-and-web-ios-nlos-adapter-boundary.md b/docs/adr/ADR-330-native-and-web-ios-nlos-adapter-boundary.md new file mode 100644 index 00000000..14fb2494 --- /dev/null +++ b/docs/adr/ADR-330-native-and-web-ios-nlos-adapter-boundary.md @@ -0,0 +1,288 @@ +# ADR-330: Native and web iOS NLOS adapters and Apple API boundary + +| Field | Decision | +|---|---| +| **Status** | Accepted; native and web software surfaces are implementable now, direct built-in iPhone transient-NLOS remains unsupported until a documented Apple API and live device evidence exist | +| **Date** | 2026-08-22 | +| **Owners** | RuView Labs iOS, mobile web, API, security, and sensing maintainers | +| **Scope** | Swift package/app, ARKit context adapter, authenticated track transport, web replay/live UI, capability claims | +| **Depends on** | ADR-295, ADR-305, ADR-319, ADR-328, ADR-329 | +| **Related** | ADR-034, ADR-035, ADR-331 | +| **Implementation** | `ui/ios-nlos`, `ui/mobile`, `ruview.nlos.track.v1` | + +## Context + +The phrase “smartphone-grade LiDAR” describes a performance/cost class; it does +not guarantee that an App Store process can read every internal sensor signal. +The MIT technique needs each SPAD zone's photon-arrival histogram. Apple +documents ARKit APIs for processed +[`sceneDepth`](https://developer.apple.com/documentation/arkit/arframe/scenedepth), +[`smoothedSceneDepth`](https://developer.apple.com/documentation/arkit/arframe/smoothedscenedepth), +world tracking, and +[`sceneReconstruction`](https://developer.apple.com/documentation/arkit/arworldtrackingconfiguration/scenereconstruction). +Apple's [scene-depth point-cloud sample](https://developer.apple.com/documentation/arkit/displaying-a-point-cloud-using-scene-depth) +shows how applications request and unproject processed depth. These public +surfaces do not document the per-zone transient histogram used by the MAS +measurement model. + +This is an API assessment, not a claim about undisclosed Apple hardware or +future operating systems. It must be reviewed against official documentation +for each supported iOS/Xcode release. Until the needed signal is documented and +validated on a physical device, RuView must not present ARKit depth as an MIT +NLOS reproduction. + +The user still needs two useful iOS surfaces: + +1. a native app that compiles a shared contract, uses public ARKit depth/pose for + visible context, and consumes external RuView NLOS tracks; and +2. a web-capable mobile UI that receives authenticated tracks or deterministic + replay without pretending to capture the phone's LiDAR. + +## Decision + +### 1. Publish one contract with explicit capability levels + +All iOS surfaces consume `ruview.nlos.track.v1`. Capability is an enum, not +inferred from device marketing: + +| Capability | Meaning | Permitted label | +|---|---|---| +| `unavailable` | no valid track source | unavailable/unknown | +| `replay` | deterministic fixture or recorded stream | `SYNTHETIC` or `REPLAY`, persistently watermarked | +| `arkit_context` | public ARKit pose/depth/mesh only | line-of-sight context; never NLOS | +| `external_live` | authenticated live track from histogram-capable external pipeline | live external NLOS, subject to evidence/certificate | +| `apple_transient_live` | future documented raw-transient Apple adapter | disabled until separate ADR, API proof, device witness, and ADR-331 gate | + +Unknown values fail closed. UI copy names the actual source and evidence level. +It never shortens `external_live` to “iPhone sees around corners.” + +### 2. Keep the Swift core platform-neutral + +`ui/ios-nlos/Package.swift` defines: + +1. `RuViewNLOSCore`, a pure Swift contract/validation library that can be unit + tested without ARKit; and +2. `RuViewNLOSApple`, an Apple-only adapter behind `canImport(ARKit)` and runtime + availability/capability checks. + +The direct iOS app and shared `RuViewNLOS` scheme use the same validated model. +The current v1 core decoder rejects unknown/excess JSON shape, non-finite or +bounded-range numeric values, invalid covariance diagonals, expired/future +tracks, excessive arrays, duplicate IDs, and illegal provenance/evidence +transitions. It has no world-frame or capture-manifest field yet; those are +live-promotion contract requirements, not claims about this G0 scaffold. + +The current Apple adapter is deliberately a static capability probe: it does +not start an `ARSession`, request camera permission, or capture/export any depth. +It documents the public-API boundary and supports the authenticated external +track client. A later, separately reviewed line-of-sight context adapter may +export camera pose, intrinsics, visible scene depth/confidence, smoothed depth, +and mesh metadata with explicit `arkit_context` labeling. Neither implementation +synthesizes photon histograms or changes context into an NLOS evidence source. + +### 3. Treat the native app as a client of the external NLOS pipeline + +The production data path is intended to be an authenticated, versioned RuView +track endpoint, not direct access to the ST board from UI code. The current v1 +client verifies: + +1. TLS and the configured RuView service identity; +2. a scoped, revocable pairing token or short-lived session authorization; +3. authenticated server session, envelope session, and schema bindings; +4. monotonic sequence and bounded clock skew; +5. track expiry, covariance, modality contributions, and calibration hash. + +L2/live promotion additionally requires tenant/workspace authorization, +coordinate-frame and capture-manifest bindings, scoped enrolled sensor identity, +and a valid witness/capability certificate. Those fields are not silently +inferred from v1. + +Network loss, app suspension, sensor unavailability, decode error, and stale +data immediately degrade the capability +and clear or visually expire the live track. Cached data is never silently live. + +The initial Swift client further pins concrete bounds: `wss` only; no embedded +URL credentials or fragments; redirects refused; ephemeral URLSession state; a +256 KiB maximum message; an exact bounded JSON model; sequence no larger than +the JavaScript safe integer and strictly increasing within a bound session; at +most a 5 second track lifetime; and pairing tokens of 32–512 visible ASCII bytes +stored as `WhenUnlockedThisDeviceOnly` Keychain data. `SYNTHETIC` frames require +replay transport and the reserved zero calibration hash. Live provenance must +retain a raw/CNH transient kind; a nonzero calibration hash is enforced at L2 +calibrated and above, not for every L1 live envelope. These are wire/client +safety rules, not proof that the stream is physically honest. + +### 4. Web iOS consumes tickets and replay; it does not capture transients + +The NLOS surface in `ui/mobile` implements the same schema and freshness rules. +Live mode first obtains an authenticated, short-lived, single-purpose ticket +from `/api/v1/nlos/ws-ticket`, then connects to the NLOS stream. Long-lived +OAuth tokens and credentials are not placed in query strings, logs, local +storage, or replay files. The current server binds the ticket to its server +session, a 30-second expiry, and one use; the client pins the returned WSS URL +to the configured same authority. Tenant/workspace/audience/origin claims are +required before L2 deployment but are not present in the v1 ticket schema. + +Deterministic replay is a first-class developer/demo mode. It is visibly marked +`SYNTHETIC` throughout the view, uses fixed seeds/fixtures, cannot update live +spatial memory, and cannot satisfy the research gate. Bounded exact-key +validation and same-authority ticket checks are implemented. Deployment CSP, +server-enforced origin policy, and reconnect backoff remain required hardening +before live promotion. + +The web view visualizes plan/perspective geometry, covariance/quality, +freshness/expiry, modality/provenance, and disconnected/degraded state. It does +not call an undocumented WebKit or ARKit bridge. + +Both clients accept at most 1,000 ms of future clock skew and a 5,000 ms +envelope lifetime. The web profile is intentionally stricter under receive +silence: it clears a frame after 1,500 ms without a replacement even when the +publisher supplied a longer TTL. Native clears at the signed envelope expiry. +This conservative display-liveness difference does not alter wire acceptance, +evidence level, or research metrics. + +### 5. Keep pose/context fusion separate from evidence fusion + +ARKit pose may help align the phone display or a separately calibrated external +sensor. That transform is valid only when extrinsic calibration, timestamps, +coordinate conventions, and uncertainty pass. Scene depth may display the +relay wall or visible geometry. It is not added to the optical NLOS likelihood +unless a future reviewed model defines and evaluates that factor. + +If the phone and external sensor are not rigidly mounted, phone pose cannot be +treated as sensor pose without an independently measured time-varying transform. +The UI may still display both frames separately. + +### 6. Re-evaluate Apple support through a documented gate + +At each major iOS/Xcode intake, a reviewer searches Apple's official SDK headers, +documentation, entitlements, privacy manifest requirements, and App Store rules. +Direct Apple transient support advances only if all are true: + +1. a public supported API exposes timing histogram/count data with documented + units, dimensions, timestamps, and device support; +2. use requires no private symbols, jailbreak, reverse engineering, or hidden + entitlement; +3. a physical-device capture proves that the data includes usable multipath; +4. privacy/security review and user disclosure pass; and +5. the same live reproduction/fusion protocol passes under a new adapter ADR. + +No marketing article, simulator API, depth-map correlation, or successful +compile satisfies this gate. + +## Performance decision + +| Surface | Software target | Measurement note | +|---|---|---| +| Swift core | decode/validate without blocking the main actor; bounded memory | benchmark representative max-size track frames | +| Native rendering | newest-frame policy; 30 fps-capable presentation where device allows | rendering rate is not sensing rate | +| Web validation/store | bounded per-message parsing and no unbounded history | reject oversized messages before state update | +| Web rendering | responsive recent-track visualization and backoff under loss | requestAnimationFrame rate is not NLOS update rate | +| Transport | p50/p95 server-to-view latency, reconnects and dropped/stale counts | report separately from sensor-to-track latency | + +UI optimization may decimate display history, but it cannot decimate or reorder +the evidence record. Performance tests use `SYNTHETIC` fixtures and are labeled +software evidence only. + +## Security and privacy + +1. NLOS tracking can reveal a person outside direct view. Native and web apps + require explicit consent, purpose text, a persistent indicator, pause/stop, + and clear source/provenance. Background capture is disabled by default. +2. App Transport Security/TLS and short-lived scoped authorization are required. + Debug cleartext/local exceptions are not release defaults. +3. WebSocket messages are untrusted. Enforce maximum message size, schema, + numeric bounds, sequence, freshness, origin, rate, tenant, and coordinate + frame before rendering or storage. +4. Replay files contain no credentials and no raw person/CSI/transient data by + default. Fixtures are synthetic or approved/de-identified and immutable. +5. Native and web telemetry excludes positions, raw sensor frames, tokens, + calibration secrets, and stable person identifiers. Diagnostics use bounded + counters and redacted errors. +6. The display is advisory. It cannot directly actuate a device or certify that + a hidden region is safe. + +## Alternatives considered + +### Make the first milestone a direct iPhone NLOS app + +Rejected. It makes the research depend on an undocumented measurement. The +external sensor proves the architecture independently while iOS remains a +transport, context, and presentation adapter. + +### Use only a native app + +Rejected. A web/mobile view lowers review and demo friction, exercises the +versioned protocol, and can run deterministic fixtures. It still must not claim +direct sensor access. + +### Use only a web app and skip native Swift + +Rejected. ARKit pose/depth and physical-device capability checks require the +native SDK. The pure Swift core also gives an independent decoder implementation. + +### Embed a permanent bearer token in the app or WebSocket URL + +Rejected. Tokens leak through logs, browser history, proxies, crash reports, +and screenshots. Use short-lived, scoped, one-use tickets. + +### Treat replay as a transparent fallback during disconnect + +Rejected. It would misrepresent stale/synthetic state as live. Replay is an +explicit operator mode with persistent labeling and separate state. + +## Consequences + +### Positive + +1. Native and web iOS deliver useful live/replay experiences without blocking + the core research on Apple's API choices. +2. The same schema, freshness, provenance, and coordinate rules apply across + Rust, Swift, and TypeScript. +3. Public ARKit pose and visible geometry remain valuable but honestly scoped. +4. Future Apple transient access has a precise, reviewable activation gate. + +### Costs and limitations + +1. The first live iOS experience needs an external histogram sensor and RuView + host; the phone alone is not the sensing system. +2. Cross-language contract tests and release CI add maintenance. +3. Web presentation depends on an authenticated RuView backend; offline mode is + replay only. +4. Simulator and Linux Swift tests cannot validate ARKit, LiDAR hardware, + App Store behavior, or a physical iOS build. + +## Rollout and rollback + +| Phase | Enablement | Rollback trigger | Action | +|---|---|---|---| +| I0 | pure Swift/TypeScript contract and deterministic fixtures | decoder disagreement, unbounded input, provenance drift | disable view; fix contract/golden vectors | +| I1 | future separately reviewed ARKit context on supported device | permission/session/transform failure | capability `unavailable`; clear context | +| I2 | authenticated external live tracks with valid, unexpired evidence/capability certificate and privacy approval | stale/replay/auth/tenant/evidence mismatch or approval withdrawal | disconnect; clear live state; retain explicit replay option | +| I3 | future Apple transient adapter | any of five activation gates absent/regressed | remove capability certificate and adapter flag | + +The NLOS tab/app can be removed or disabled without affecting core RuView CSI, +Rust sensing, memory, MCP, or orchestration. + +## Objective acceptance mapping + +| ID | Requirement | Evidence | +|---|---|---| +| NLOS-330-01 | Swift core builds/tests without ARKit | `cd ui/ios-nlos && swift test` on supported Swift host | +| NLOS-330-02 | Native iOS scheme builds with public APIs | macOS `xcodebuild` simulator gate and physical-device smoke evidence, separately labeled | +| NLOS-330-03 | Current Apple capability probe, absent a new adapter ADR, never emits raw-transient/live-NLOS provenance | capability/provenance unit tests and source review | +| NLOS-330-04 | Web contract accepts valid `ruview.nlos.track.v1` and rejects malformed/stale/oversized input | Jest/TypeScript boundary tests | +| NLOS-330-05 | Live web transport uses short-lived authenticated tickets | client/server integration and replay/origin/expiry negative tests | +| NLOS-330-06 | Replay remains persistently `SYNTHETIC` and cannot pass live gate | UI/store tests plus harness research rejection | +| NLOS-330-07 | Missing Xcode/hardware or NLOS backend degrades honestly | advisory verifier skips and offline/disconnect UI tests | +| NLOS-330-08 | Web and native builds remain separable from core runtime | build matrix with NLOS surfaces disabled/absent | +| NLOS-330-09 | Future Apple activation uses only documented public access | official SDK/header diff, entitlement scan, privacy/App Store review, device witness, and separate adapter ADR | + +## References + +1. Apple, [`ARFrame.sceneDepth`](https://developer.apple.com/documentation/arkit/arframe/scenedepth). +2. Apple, [`ARFrame.smoothedSceneDepth`](https://developer.apple.com/documentation/arkit/arframe/smoothedscenedepth). +3. Apple, [`ARWorldTrackingConfiguration.sceneReconstruction`](https://developer.apple.com/documentation/arkit/arworldtrackingconfiguration/scenereconstruction). +4. Apple, [Displaying a point cloud using scene depth](https://developer.apple.com/documentation/arkit/displaying-a-point-cloud-using-scene-depth). +5. Somasundaram et al., [consumer-NLOS measurement model](https://arxiv.org/html/2605.17865v1). +6. STMicroelectronics, [VL53L8CH raw compact normalized histogram interface](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html). diff --git a/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md b/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md new file mode 100644 index 00000000..55f20db4 --- /dev/null +++ b/docs/adr/ADR-331-consumer-nlos-evidence-privacy-benchmark-metaharness.md @@ -0,0 +1,331 @@ +# ADR-331: Consumer NLOS evidence levels, privacy/security, benchmarks, and optional MetaHarness governance + +| Field | Decision | +|---|---| +| **Status** | Accepted; software gates are implemented independently of live-hardware evidence, research promotion remains blocked until the preregistered capture passes | +| **Date** | 2026-08-22 | +| **Owners** | RuView Labs maintainers, security/privacy reviewers, benchmark owners | +| **Scope** | Claim taxonomy, acceptance records, benchmark governance, contributor harness, release/promotion and rollback | +| **Depends on** | ADR-166, ADR-168, ADR-282, ADR-295, ADR-299, ADR-303 through ADR-305, ADR-318/319, ADR-328 through ADR-330 | +| **Implementation** | `harness/ruview`, `docs/security/consumer-nlos-threat-model.md`, `docs/research/consumer-nlos-acceptance-protocol.md` | + +## Context + +Consumer NLOS is unusually easy to overstate. A point-cloud animation can look +plausible even when driven by replay, stale state, calibration leakage, direct +line of sight, or a reflectivity regime unlike the deployment target. Native and +web builds can validate contracts without measuring one multipath photon. CSI +and LiDAR may also share temporal or labeling leakage, producing an apparent +fusion gain that contains no independent RF information. + +The capability reveals presence and trajectories outside direct view, which +raises privacy and misuse risks despite not producing a conventional photograph. +It also has no present safety case for autonomous actuation. The project needs a +promotion gate that separates source integrity, software validation, controlled +laboratory evidence, field generalization, and production evidence. + +The existing RuView contributor harness is dependency-free at runtime and uses +MetaHarness/Flywheel/Darwin only as development aids. NLOS verification should +reuse that posture: helpful, deterministic, and removable, never a requirement +for sensing, fusion, memory, routing, MCP, or normal runtime behavior. + +## Decision + +The `@ruvnet/ruview` packed-size ceiling increases from 160 KiB to 220 KiB to +carry the bounded NLOS contract verifier, research-gate evaluator, and operator +skill. The existing no-source-map, no-runtime-dependency, tarball smoke, and +claim-honesty gates remain mandatory. This is a reviewed capability budget, not +an unbounded exemption; the current dry-run package is approximately 203 KiB. + +### 1. Keep three orthogonal labels + +Every NLOS result records: + +1. **Source provenance**: exactly one logical value for live hardware, replay, + or synthetic input; unknown is never coerced to live. The acceptance record + encodes these as `LIVE_HARDWARE`/`REPLAY`/`SYNTHETIC`, while the + `ruview.nlos.track.v1` wire contract uses `live`/`replay`/`synthetic`. +2. **Claim tag**: `MEASURED` with a named reproducer/manifest, `CLAIMED` with a + primary external source, or `SYNTHETIC` for simulation/fixtures. A build is + “validated software,” not a measured sensing result. +3. **ADR-282 evidence level**: + +| Level | Meaning for NLOS | +|---|---| +| L0 | simulation or generated transient/track fixtures only | +| L1 | captured replay; deterministic pipeline behavior, not a fresh live result | +| L2 | controlled laboratory capture with external ground truth and frozen protocol | +| L3 | held-out room plus target/subject validation with leakage-resistant splits | +| L4 | multi-site field pilot under approved privacy/safety operations | +| L5 | production operational evidence, incident monitoring, drift and rollback history | + +The upstream paper is cited as `CLAIMED` in RuView until reproduced. The first +RuView live acceptance can reach L2 only. No amount of L0/L1 replay volume +upgrades a capability to L2. + +This ADR-282 maturity level is not the similarly named wire +`evidenceLevel` in `ruview.nlos.track.v1`. V1 accepts +`l0_synthetic`/`l1_measured`/`l2_calibrated`; `l3_corroborated` is reserved for +a future contract that can retain authenticated dual-modality lineage. These +values describe one envelope's source/calibration ceiling. They never self-promote +research maturity: a synthetic envelope is ADR L0, a captured replay remains +ADR L1 regardless of its historical wire label, and a live +`l2_calibrated` envelope reaches ADR L2 only through this +frozen external-ground-truth witness protocol. The acceptance JSON therefore +uses separate `claim_tag: MEASURED` and ADR maturity `evidence_level: L2`. + +### 2. Separate software, research, and release gates + +| Gate | What can pass it | What it proves | What it does not prove | +|---|---|---|---| +| Software | Rust/Swift/TypeScript unit, property, contract, build, replay and security tests | implementations compile and enforce declared invariants | photon capture, NLOS accuracy, physical iPhone support | +| Research | preregistered `LIVE_HARDWARE` capture with external ground truth | objective reproduction and fusion endpoints for named strata | field generalization, safety, identity, production readiness | +| Release/promotion | software + security + privacy + required evidence/certificate + human review | capability may be exposed at its exact evidence level | authority to actuate or claim a higher level | + +An unavailable Xcode/hardware toolchain is an explicit `SKIPPED`, not a pass or +failure. A present partial/incompatible surface fails shallow discovery. If no +toolchain executes, the verifier reports `NO_BUILD_TOOLCHAINS_AVAILABLE`, never +a pass. Available-build success is only a subset of Gate A and is forbidden in +a hardware or release claim. + +### 3. Preregister the reproduction and fusion endpoint + +Before opening the test partition, freeze: + +1. upstream commit and firmware/configuration; +2. sensor/CSI identity, calibration and coordinate transforms; +3. scene/target strata, exclusion rules and direct-line-of-sight checks; +4. randomized sequence order and grouped split manifest; +5. background capture and OOD/freshness thresholds; +6. track initialization, lost-track definition and maximum association gap; +7. primary metrics and bootstrap confidence interval procedure; and +8. all model/weight/threshold versions for LiDAR-only and fused arms. + +The hard gate requested for this program is: + +1. **Reproduction:** LiDAR-only produces at least 27 accepted end-to-end track + updates per second on live hardware, the objective definition of roughly + 30 fps. +2. **Fusion:** over at least 100 paired live sequences, fusion achieves + +\[ +G_e = \frac{E_L-E_F}{E_L} \ge 0.25 +\quad\text{or}\quad +G_\ell = \frac{\ell_L-\ell_F}{\ell_L} \ge 0.25, +\] + +where \(E\) is the preregistered mean target-position error and \(\ell\) is the +preregistered lost-track rate. Both metrics are reported even if only one is +the success endpoint. Zero denominators do not count as improvement. + +The protocol also reports confidence intervals, p95 position error, time to +first lock, false tracks in an empty hidden volume, update/latency distribution, +frame loss, calibration/OOD rejection, and performance per reflectivity, range, +motion, relay surface, room, and RF geometry. These secondary metrics prevent a +single successful aggregate from hiding unacceptable behavior. + +### 4. Use a bounded, reviewable acceptance record + +`ruview.nlos.acceptance.v1` is repository-contained JSON that references, but +does not embed, sensitive captures. Required fields include: + +1. exact schema, `LIVE_HARDWARE` provenance, `MEASURED` claim tag, L2 evidence + level, and `EXTERNAL` ground truth; +2. protocol-frozen-before-capture flag; +3. enrolled external ST VL53L8-series sensor-model label for v1, verified raw/CNH transient kind, full + upstream SHA-1, and SHA-256 digests for protocol, firmware/API, combined and + CSI capture manifests, scoped enrolled identities, calibration and analysis; +4. zero synthetic frames and zero replay frames in the scored set; +5. independently verified CSI, a CSI-only ablation, at least one CSI source and + at least 100 paired sequences; +6. raw aggregate counts/durations/sums from which each arm's update rate, mean + position error, lost-track rate, offered optical rate and fused optical-frame + loss are recomputed; full paired-sequence position coverage and shared + endpoint denominators are mandatory; and +7. preregistered bootstrap seed digest, at least 10,000 resamples, + multiplicity-adjusted intervals, witness review, LOS exclusion, and passed + privacy/security review. + +The advisory verifier validates an exact-key record, bounds, provenance, +digests, arithmetic, and thresholds; unknown fields fail so secrets or raw data +cannot silently hitchhike in the acceptance artifact. It cannot independently +prove that a digest corresponds to an honest physical experiment. Human reviewers inspect the immutable manifest, +external ground-truth synchronization, raw-capture access controls, exclusions, +and analysis reproducer before promotion. + +### 5. Extend, but do not require, the RuView contributor harness + +The dependency-free `@ruvnet/ruview` CLI/MCP registry adds governed advisory +surfaces. MCP/static inspection remains read-only. Local `--run-builds` is an +explicit execution mode, must target a trusted checkout, uses an allowlisted +child environment and redacted bounded tails, and is not a sandbox: + +| Surface | Behavior | +|---|---| +| `ruview nlos plan` / `ruview_nlos_plan` | returns four staged phases, measurement invariant, expected surfaces and exit gates | +| `ruview nlos verify` | shallowly discovers expected Rust/native/web manifests/contracts, can explicitly run available local builds in a trusted checkout, and evaluates an optional live acceptance record | +| `ruview_nlos_verify` | read-only MCP inspection of the auto-detected repository and optional repository-confined evidence; rejects repository selection and build execution | +| `consumer-nlos` skill | contributor playbook for measurement boundary, reproduction, fusion, evidence, security, and iOS limitations | + +The verifier discovers `v2/crates/ruview-nlos`, `ui/ios-nlos`, and `ui/mobile`. +If an optional surface is absent, it reports `ABSENT`. Once its feature marker +exists, missing, escaping, oversized or incompatible required artifacts are +`MALFORMED` and fail. String/manifest discovery does not replace contract tests. +Evidence files are regular, bounded, repository-confined JSON; path escape, +symlink escape, oversize, malformed JSON, replay, or synthetic input fails. + +MetaHarness, Ruflo, Darwin, and Flywheel remain contributor tooling. Direct +crate/app builds without the harness are the evidence that runtime does not +require it; the verifier's status field is not proof by itself. Harness +proposals cannot modify sensing evidence, promote a +model, publish a package, merge code, or authorize hardware. Promotion retains +human review. + +For future harness-policy evolution, a proposal is retained only if a frozen +holdout shows more than 2 percent quality lift, contributor cost regression is +below 1 percent, p95 latency regression is below 5 percent, security/legacy +tests do not regress, provenance is verified, and a human approves it. These +thresholds govern contributor tooling only; they do not replace the 25 percent +sensor-fusion endpoint. + +### 6. Apply privacy-by-default controls + +1. A named controller/operator records purpose, lawful/organizational basis, + approved spaces, experiment window, access list, retention, and deletion. +2. Visible notices and an active sensing indicator are required. Participants + can pause/withdraw where applicable. No hidden deployment is allowed. +3. Raw transient, CSI, ground-truth video/tag data, and joined trajectories are + P0/P1 research data: encrypted locally, access logged, minimized, separated + by tenant/experiment, and deleted on the frozen schedule. They are never + committed to Git or emitted in harness logs. +4. Default outputs are bounded session-scoped tracks with covariance, + provenance and expiry. No face image is needed, but “camera-free” is not + “privacy-free.” No biometric identity or cross-session re-identification is + claimed or enabled by default. +5. Dataset/model publication needs a separate disclosure/re-identification + review, consent/license check, and removal request path. +6. Safety and high-consequence use are out of scope. Tracks are advisory and + never direct actuation authority. + +### 7. Make security a release blocker + +The threat model in `docs/security/consumer-nlos-threat-model.md` is mandatory. +Release requires no confirmed high/critical finding across: + +1. untrusted transient/track/parser boundaries and numeric/resource exhaustion; +2. sensor, session, calibration and capture identity/provenance; +3. replay/stale/future/duplicate and cross-tenant/world-frame attacks; +4. transport authorization, short-lived web tickets, TLS/origin and secret + handling; +5. firmware/upstream/npm/Cargo/Swift supply chain and license review; +6. raw research-data retention, logs, fixtures, telemetry and repository policy; +7. denial/degradation behavior and lack of direct actuation. + +Findings are confirmed with focused tests before remediation claims. Automated +scanner output alone is not a confirmed vulnerability or a cleared release. + +## Performance and optimization governance + +Performance reports name hardware, OS/toolchain, configuration, capture digest, +commit, warmup, sample count, and statistics. Report sensor capture rate, +accepted frame rate, track update rate, sensor-to-track latency, service-to-view +latency, CPU, peak memory, queue drops, and output quality separately. + +Optimization sequence: + +1. profile parser, normalization, rendering/likelihood, resampling, temporal + join, serialization, and UI independently; +2. add scalar golden vectors and property tests before vectorization/caching; +3. bound buffers/particles/history and prefer newest-frame backpressure; +4. compare LiDAR-only and fused quality after every material optimization; and +5. roll back any change that violates numerical tolerance, security, privacy, + freshness, or the preregistered endpoint. + +Configured 30 Hz, display 30/60 fps, and replay throughput are not equivalent +to live end-to-end tracking. Reports must name which one was measured. + +## Alternatives considered + +### Allow synthetic evidence to pass when hardware is unavailable + +Rejected. Synthetic and replay are essential for software QA but cannot measure +the required optical path or independent RF gain. + +### Treat the published MIT result as RuView's baseline evidence + +Rejected. It is a primary `CLAIMED` reference with different hardware/scenes and +does not validate RuView's integration, privacy, security, or fusion. + +### Make MetaHarness a required runtime coordinator + +Rejected. Core sensing, fusion, memory, routing, MCP and UI must operate without +development orchestration. An advisory tool cannot be a physical trust anchor. + +### Store all raw captures indefinitely for reproducibility + +Rejected. Reproducibility uses controlled access, immutable digests, frozen +manifests and a retention schedule. Indefinite person/space/RF data creates +disproportionate risk. + +### Promote on the 25 percent point estimate alone + +Rejected. Pairing, confidence intervals, strata, secondary harms, calibration, +provenance, security, privacy and human review remain required. + +## Consequences + +### Positive + +1. Reviewers can distinguish compile/replay success from physical NLOS evidence. +2. The 30 fps and 25 percent claims become executable, preregistered gates. +3. Privacy and security are part of experiment design, not a post hoc checklist. +4. The optional harness makes missing toolchains/surfaces explicit without + weakening the independent runtime. + +### Costs and limitations + +1. Controlled capture, external ground truth, review and data governance take + more time than a visual demo. +2. The acceptance JSON verifies integrity/arithmetic, not physical truth by + itself; witness review remains necessary. +3. L2 success does not establish L3–L5 generalization or safety. +4. A negative fusion result blocks promotion even if each modality is + individually interesting. + +## Rollout and rollback + +| Stage | Published capability | Required evidence | Rollback | +|---|---|---|---| +| G0 | development preview | L0 synthetic software fixtures, explicitly labeled | disable flag/view/tool; retain tests | +| G1 | captured replay demo | L1 capture manifest and privacy approval | revoke fixture/access; return to synthetic | +| G2 | controlled research result | L2 live protocol + acceptance + security/privacy review | invalidate certificate; remove live label; optical-only or unknown | +| G3+ | held-out/pilot/production | level-specific ADR-282 artifacts and monitoring | level downgrade, stop capture, delete per schedule, incident review | + +Rollback preserves audit/witness records and never rewrites provenance. It may +disable NLOS/fusion independently while CSI and the rest of RuView continue. + +## Objective acceptance mapping + +| ID | Requirement | Evidence | +|---|---|---| +| NLOS-331-01 | Claim tag, provenance and L0–L5 level never alias | type/state tests, UI label tests, claim checker | +| NLOS-331-02 | Synthetic/replay cannot pass research | `evaluateResearchEvidence` negative tests and CLI nonzero with `--require-research-pass` | +| NLOS-331-03 | Reproduction is roughly 30 fps | live accepted update rate >=27 Hz, external-ground-truth manifest | +| NLOS-331-04 | Fusion has objective value | `MEASURED` in the capture-manifest witness report under the frozen protocol over >=100 paired sequences; >=25% mean-error **or** lost-track reduction with adjusted interval excluding zero, shared pairing digest, and all frozen guardrails passing | +| NLOS-331-05 | Harness remains optional | core/app tests without harness dependencies; absent-surface test | +| NLOS-331-06 | Present malformed surface/evidence fails closed | path/bounds/schema/digest/provenance and partial-surface tests | +| NLOS-331-07 | Privacy controls cover collection through deletion | approved experiment record, access/retention/deletion audit | +| NLOS-331-08 | No unaccepted high/critical security finding at release | threat-model review, focused regression tests and dependency/secret scans; `NLOS-SEC-EX-001` was closed by lockfile remediation without an exception, while future findings still require correction or an exact signed, unexpired exception record | +| NLOS-331-09 | Optimization does not trade away quality or freshness | golden/reference equivalence and named benchmark report | +| NLOS-331-10 | No hardware/App Store claim from CI alone | PR/release claim check and explicit physical-device witness field | +| NLOS-331-11 | Optional harness-policy evolution cannot self-promote or regress its frozen gates | `MEASURED` frozen held-out/anchor report proving >2% quality lift, <1% cost regression, <5% p95 latency regression, unchanged security/legacy gates, verified provenance and human approval; otherwise discard proposal | + +## References + +1. ADR-282: mandatory L0–L5 evidence ladder. +2. ADR-295: source provenance state machine. +3. ADR-303/304/305: ground-truth synchronization, evidence engine, authenticated sensor identity. +4. ADR-318/319: capability certificates and witness chain. +5. MIT, [Consumer NLOS project and 30 Hz demonstration](https://cornar.media.mit.edu/). +6. Somasundaram et al., [Nature paper](https://doi.org/10.1038/s41586-026-10502-x). +7. STMicroelectronics, [VL53L8CH raw histogram interface](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html). diff --git a/docs/research/consumer-nlos-acceptance-protocol.md b/docs/research/consumer-nlos-acceptance-protocol.md new file mode 100644 index 00000000..5b55175c --- /dev/null +++ b/docs/research/consumer-nlos-acceptance-protocol.md @@ -0,0 +1,545 @@ +# Preregistered RuView consumer NLOS reproduction and CSI-fusion protocol + +**Protocol ID:** `ruview-consumer-nlos-v1` +**Status:** Template to freeze before the first scored capture +**Governed by:** ADR-328, ADR-329, ADR-330, ADR-331 +**Primary claim scope:** controlled, identity-free tracking of a known hidden +rigid target using an externally enrolled, histogram-capable ST transient sensor +whose exact silicon/firmware/API are recorded; not built-in iPhone +NLOS, through-wall optical sensing, unrestricted people tracking, or safety use + +## 1. Research question and decision rule + +This protocol answers two ordered questions: + +1. Can the pinned consumer-NLOS baseline track a hidden target from live + commodity transient LiDAR at roughly 30 frames per second? +2. After that is established, does independently captured RuView CSI improve + target-position error or lost-track rate by enough to justify fusion? + +The program passes only when all provenance/privacy/security gates pass and: + +1. the LiDAR-only arm emits at least **27 accepted end-to-end track updates per + second** on live hardware; and +2. over at least **100 paired live sequences**, the fused arm reduces either + mean target-position error or lost-track rate by **at least 25 percent** + relative to LiDAR-only, with its adjusted interval excluding zero and all + frozen guardrails passing. Fused update rate remains a reported guardrail, + not an added substitute for the requested improvement endpoint. + +`SYNTHETIC`, simulator, generated, duplicated, or captured replay frames cannot +pass. They may be used for software QA and pilot power planning only. A build, +UI frame rate, configured sensor frequency, or upstream paper result is not the +measured endpoint. + +## 2. Hypotheses + +### H1: live reproduction + +For the primary controlled stratum, the LiDAR-only pipeline's accepted +sensor-to-track update rate is at least 27 Hz. Failure stops confirmatory fusion +interpretation. Diagnostics may continue but are labeled exploratory. + +### H2a: position-error improvement + +\[ +G_e = \frac{\bar E_L - \bar E_F}{\bar E_L} \ge 0.25, +\] + +where \(\bar E_L\) and \(\bar E_F\) are sequence-weighted mean Euclidean target +position errors for LiDAR-only and fused arms, using an external ground-truth +coordinate frame. + +### H2b: lost-track improvement + +\[ +G_\ell = \frac{\ell_L - \ell_F}{\ell_L} \ge 0.25, +\] + +where \(\ell\) is the fraction of evaluable time that satisfies the frozen +lost-track definition. + +Fusion succeeds if H2a **or** H2b meets its magnitude and multiplicity-adjusted +uncertainty gate. Both metrics and all guardrails are reported. A zero baseline +denominator cannot establish gain. + +## 3. Roles and separation of duties + +| Role | Responsibility | Must not do | +|---|---|---| +| Protocol owner | freeze protocol, strata, splits and decision rule | inspect sealed confirmatory results before freeze | +| Capture operator | approved setup, consent, identity/calibration, run manifest | tune model/thresholds during scored capture | +| Ground-truth owner | independent system, clock/transform checks, sealed labels | feed labels into online LiDAR or fusion arms | +| Model owner | freeze upstream/Rust/CSI/fusion artifacts | alter artifacts after test partition opens | +| Analyst | run committed reproducer and report all endpoints/strata | delete trials or change exclusions post hoc | +| Security/privacy reviewer | approve collection, access, retention, threats | waive live provenance or identity/actuation boundaries | +| Witness reviewer | verify digests, randomization, exclusions and analysis | equate acceptance JSON arithmetic with physical audit | + +One person may hold multiple roles in a pilot, but protocol/model ownership and +ground-truth/confirmatory analysis should be independently reviewed. + +## 4. Hardware and software freeze + +Complete and sign this table before scored capture: + +| Item | Frozen value | +|---|---| +| ST kit and sensor | exact board and silicon read from hardware; raw/CNH API compatibility witnessed; scoped enrollment/certificate reference stored in the restricted manifest rather than a guessable raw-serial hash | +| Firmware | source/release, compiler/toolchain, binary SHA-256 | +| Sensor configuration | zones, bins, bin width, requested rate, integration/subsampling, ambient settings | +| Upstream baseline | `sidsoma/consumer-nlos` full commit SHA and clean/patch manifest | +| RuView | full Git commit SHA; `ruview-nlos` crate feature/config digest | +| CSI nodes | authenticated IDs, hardware/firmware, channel/bandwidth/subcarrier configuration | +| Optical calibration | wall points/plane, direct-return masks/peaks, background, timestamps, SHA-256, expiry | +| RF calibration | room/link fingerprint, coordinate transform, timestamp, SHA-256, expiry/OOD threshold | +| Ground truth | device/camera/tag firmware/software, calibration digest and measured clock uncertainty | +| Models | canonical target response, particle count/motion prior/score, CSI model, fusion weights and digests | +| Hosts | CPU/GPU/RAM/OS, power mode, process priority, compiler/runtime versions | +| Analysis | script/lockfile/container digest, bootstrap seed list and report template | + +Changing any frozen item starts a new protocol version or invalidates the +affected block. No silent patch is permitted. + +## 5. Physical setup and primary stratum + +### 5.1 Geometry + +1. Mount the transient sensor so a matte, light-colored planar relay surface + fills its field of view. +2. Place an opaque occluder so no sensor zone, phone camera, or operator-facing + optical path directly sees the scored target. Record a setup photograph/mesh + for review; do not publish participant imagery by default. +3. Start with the upstream-friendly geometry: sensor-to-wall less than 1 m and + wall-to-target approximately 1 to 1.5 m, then record exact distances. +4. Define a right-handed world coordinate frame, units in metres, transform + chain, uncertainties, and a hidden-region boundary before capture. +5. Place CSI nodes/APs in a fixed documented configuration. Confirm CSI is not + derived from the LiDAR, target-control signal, or ground-truth system. + +### 5.2 Primary target + +The confirmatory reproduction target is a known rigid approximately 25 cm +retroreflective patch or the exact upstream canonical target. The target shape +and response are frozen before the confirmatory split. This supports a scoped +known-shape tracking claim only. + +Diffuse rigid objects, hands/people, multiple objects, other sizes, longer +ranges, sunlight, non-planar relay surfaces and moving sensors are separate +exploratory or later confirmatory strata. Never pool them to imply generalized +human/scene reconstruction. + +### 5.3 Ground truth + +Use an independent externally calibrated system, for example an overhead camera +with a rigid AprilTag/active marker or a surveyed motion stage. The system must +observe the target while remaining unavailable to the online algorithms. Measure +spatial transform error and clock offset/jitter before and after each block. + +Ground-truth labels remain sealed until capture and all three online arms are frozen. +A label interpolated beyond the frozen maximum gap makes that time point +unevaluable; it is not imputed from the NLOS output. + +## 6. Calibration and negative controls + +For every block: + +1. enroll/verify sensor and CSI identities; +2. fit the relay wall and verify per-zone direct-return distance against a + physical measurement; +3. capture the frozen-duration empty-scene optical background with no person or + target in the hidden region; +4. capture RF empty-room calibration under the approved protocol; +5. verify clocks, transform chain and calibration digests; +6. run an empty hidden-region negative sequence; +7. run a direct-line-of-sight exclusion check with the occluder; and +8. mark calibration `VALID` only after all bounds pass. + +Negative controls include sensor disconnected, CSI disconnected, stale/replayed +frame injection in a non-scored software run, target absent, static distractor, +and calibration mismatch. The live confirmatory stream contains no injected +synthetic/replay frames. + +## 7. Trial unit, sample size and randomization + +### 7.1 Paired sequence + +A paired sequence is one continuous, live, preregistered target trajectory whose +accepted transient-histogram and CSI frames are delivered simultaneously to frozen +online arms: + +1. **L:** LiDAR-only MAS tracker; CSI is unavailable to every decision in this + arm; and +2. **C:** CSI-only ablation, reported to establish independent RF information; + it need not satisfy a centimetre-localization promotion threshold; and +3. **F:** the same optical inputs and initialization plus the frozen calibrated + CSI likelihood. + +All three arms use the same synchronized live interval; L/F share the offered +optical fan-out and C/F share the offered CSI fan-out. They do not take turns on +different captures. One-arm drops remain outcomes, not exclusions. If resource contention is material, run them +on matched isolated hosts fed by the authenticated live fan-out and record fan- +out latency/drop parity. Replaying a recording later does not satisfy the live +gate. + +### 7.2 Minimum and power + +Capture at least 100 valid paired sequences in the primary stratum. Before +confirmatory capture, use a disjoint pilot or synthetic/replay data to estimate +cluster variance and document power for detecting a 25 percent gain at family- +wise alpha 0.05. If the calculated requirement exceeds 100, use the larger +number. Pilot sequences, rooms and target paths do not enter confirmatory +metrics. + +Each sequence duration, initialization window and trajectory family is frozen. +Include translation directions, speeds and positions across the hidden volume, +not 100 copies of one favorable path. Randomize trajectory order and block order +with a committed seed. Counterbalance any host assignment. + +### 7.3 Grouping and splits + +Calibration/training, pilot and confirmatory partitions are grouped by capture +session, time block, path family, target instance, room and sensor configuration. +Adjacent frames from one sequence cannot cross partitions. If a learned CSI +model uses LiDAR supervision, no optical target/embedding from the confirmatory +partition is used for training, thresholding, normalization or early stopping. + +## 8. Online quality and exclusion rules + +Freeze numeric values for each placeholder before capture: + +| Rule | Frozen value | +|---|---| +| Valid calibration age and OOD bounds | `` | +| Maximum optical/CSI/ground-truth clock uncertainty | `` ms | +| Maximum optical-to-CSI pairing skew | `` ms | +| Maximum ground-truth interpolation gap | `` ms | +| Track association radius | `` m | +| Lost-track consecutive interval | `` frames or ms | +| Posterior quality/entropy/effective-particle threshold | `` | +| Saturation/underexposure and minimum valid zones | `` | +| Maximum sequence frame-loss fraction | `` | +| Warmup/initialization exclusion | `` frames, applied to all three arms | + +Pre-capture exclusions only: + +1. consent/safety/indicator failure; +2. sensor/CSI/ground-truth identity or clock failure; +3. calibration invalid before sequence start; +4. direct line of sight or physical setup outside tolerance; +5. target controller/ground truth did not execute the randomized trajectory; +6. raw capture corruption affecting all affected arms; or +7. host failure prevents paired operation. + +Algorithm failure, low signal, lost track, high error, drift, overload, one-arm +drop, poor target position, unfavorable reflectivity or unexpected but in-scope +motion are outcomes, not exclusions. Report all excluded sequences with reason +and arm-independent timing. + +## 9. Endpoint definitions + +### 9.1 Accepted update rate + +For each sequence and arm, use the entire frozen evaluable wall-time window: + +\[ +f = \frac{N_{valid,new}}{T_{evaluable}}, +\] + +where `valid,new` means a newly computed, schema-valid, calibrated, fresh track +from a unique live sensor frame after the symmetric warmup. Duplicates, replay, +late/stale frames, renderer frames and cached outputs do not count. Report +the accepted sensor-frame count as well and require track updates not to exceed +it. If an implementation instead uses first-to-last span, its estimator is +`(N-1)/(t_last-t_first)`, not `N/span`. Report sequence distribution and overall accepted updates divided by evaluable wall +time. H1 and the fused performance guard use the lower preregistered aggregate +definition, not the maximum instantaneous rate. + +### 9.2 Position error + +At each evaluable matched timestamp: + +\[ +e_{a,t}=\|\hat p_{a,t}-p^{GT}_t\|_2. +\] + +Compute a mean within each sequence first, then the equally weighted mean across +sequences so long sequences do not dominate. Report median, p95 and axis-wise +error as secondary metrics. Invalid/missing intervals contribute to lost-track +rate and cannot simply disappear from the report. For the confirmatory position +endpoint, every paired sequence receives a score in both arms. A sequence with +no valid position receives the preregistered worst-case/censoring penalty; it is +not dropped. Consequently each arm's `position_error_sample_count` must equal +`paired_sequences`, and the shared scoring-mask/penalty rules are bound by +`endpoint_pairing_sha256`. + +### 9.3 Lost-track rate + +A track is lost when the arm has no `VALID` matched hypothesis within the frozen +association radius for at least the frozen consecutive interval after warmup. +Lost-track rate is lost evaluable time divided by total evaluable time. Report +number/duration of episodes and reacquisition time. Track-ID changes without +position loss are reported separately. + +### 9.4 Empty-region false tracks and safety guardrails + +Report confident-track time and event count during preregistered empty-region +sequences. Also report p95 latency, frame loss, calibration/OOD rejection and +non-winning primary metric. A fusion gain accompanied by materially worse empty- +region false tracks, severe latency/update-rate loss, provenance failure or a +privacy/security failure is not promoted even if the 25 percent arithmetic +passes. `offered_optical_frame_count` is the number of eligible optical frames +offered to both paired arms. Its rate is recomputed against the shared +LiDAR/fused evaluable duration and cannot exceed `sensor_configured_max_hz`. +Fused frame loss is recomputed as +`(offered_optical_frame_count - fused.accepted_sensor_frame_count) / +offered_optical_frame_count`; a supplied rate that differs fails closed. + +## 10. Statistical analysis + +1. Calculate paired per-sequence deltas and relative gains. Do not treat frames + within a sequence as independent samples. +2. Use a paired cluster bootstrap over sequences with at least 10,000 resamples + and committed seeds. Report point estimate and two-sided interval for every + metric. +3. Because H2a/H2b are alternative success endpoints, control family-wise error + with Bonferroni-adjusted 97.5 percent confidence intervals or a frozen + equivalent procedure. The successful endpoint needs point gain at least 25 + percent and its adjusted interval must exclude zero improvement. +4. Report all primary/secondary/stratified results, exclusions and missingness. + No optional stopping; capture count is frozen by power/minimum before the + confirmatory set opens. +5. Sensitivity analyses vary the frozen association/lost-track thresholds only + as clearly labeled exploratory analysis after the primary result. + +If only one endpoint passes, state exactly which one. Do not summarize it as +“25 percent more accurate” when the passing endpoint was lost-track rate. + +## 11. Execution sequence + +### Gate A: software before participants/live capture + +1. Run the NLOS Rust unit/property/golden tests and workspace gate. +2. Run Swift core tests and macOS iOS simulator build where available. +3. Run mobile web tests, typecheck, lint and web export. +4. Run harness tests/security/brain/flywheel/manifest/package gates. +5. Run secret/raw-data/dependency/license scans and close confirmed high/critical + findings. +6. Exercise deterministic `SYNTHETIC` replay, stale/disconnect/oversize/replay + rejection and empty fixture. Label all outputs software/L0. + +Convenience check: + +```bash +cd harness/ruview +node bin/cli.js nlos verify --repo ../.. --run-builds +``` + +This reports only the available build/discovery subset, with explicit skips. It +cannot pass full Gate A, Gate B, or a release gate by itself; steps 1–6 remain +required. + +### Gate B: upstream live reproduction + +1. Freeze/sign protocol, privacy approval, identities, software/hardware table, + randomization and analysis container. +2. Inspect geometry and direct-line-of-sight exclusion. +3. Calibrate optical/RF/ground truth and run negative control. +4. Execute live primary-stratum sequences with LiDAR-only online output and + ground-truth labels sealed. +5. Verify accepted end-to-end update rate >=27 Hz and report tracking/error/ + empty-region diagnostics. +6. If Gate B fails, stop confirmatory fusion interpretation; fix path under a + new protocol version. + +### Gate C: paired live fusion + +1. Freeze the CSI likelihood, fusion weights, arm fan-out and host assignment. +2. Execute at least the powered minimum of paired randomized live sequences. +3. Open ground truth once capture/artifacts/exclusions are immutable. +4. Run the committed analysis, bootstrap and stratified report. +5. Retain fusion only if update-rate, 25 percent endpoint, provenance, + privacy/security and guardrail review pass. + +### Gate D: Apple/iOS claim + +Native/web software may ship as external-track clients after Gate A. A claim that +built-in Apple LiDAR performs transient NLOS needs a separate adapter ADR, +documented public histogram API, physical-device capture, privacy/App Store +review and fresh Gates B/C. ARKit scene depth alone cannot enter Gate B. + +## 12. Required artifacts + +| Artifact | Contains | Excludes | +|---|---|---| +| Frozen protocol | signed/versioned text, hypotheses, thresholds, seeds | post-result edits | +| Capture manifest | content digests, identities, configurations, timing, strata, exclusions | raw credentials/private keys | +| Raw store | encrypted transients/CSI/ground truth under access/retention policy | Git/package/harness inclusion | +| Split manifest | grouped calibration/pilot/confirmatory IDs and hash | participant identity in public artifact | +| Analysis reproducer | locked dependencies, script, seeds, exact tables/plots | manual spreadsheet-only results | +| Security/privacy record | approvals, threat verification, retention/deletion | blanket “camera-free is safe” claim | +| Acceptance JSON | bounded aggregate fields and SHA-256 references | raw trajectories/sensor data/tokens | +| Witness report | reviewer checks and exact evidence level | higher-level field/production implication | + +## 13. Acceptance JSON + +The repository-contained record consumed by `ruview nlos verify` has this exact +top-level and per-arm key set. Unknown fields are rejected; values below are +placeholders, not results: + +```json +{ + "schema": "ruview.nlos.acceptance.v1", + "source": "LIVE_HARDWARE", + "claim_tag": "MEASURED", + "evidence_level": "L2", + "ground_truth": "EXTERNAL", + "protocol_frozen_before_capture": true, + "witness_reviewed": true, + "privacy_review_passed": true, + "security_review_passed": true, + "los_exclusion_verified": true, + "independent_csi_verified": true, + "sensor_model": "", + "transient_kind": "COMPACT_NORMALIZED_HISTOGRAM", + "sensor_configured_max_hz": "", + "upstream_commit": "", + "protocol_sha256": "", + "capture_manifest_sha256": "", + "sensor_identity_sha256": "", + "calibration_sha256": "", + "firmware_sha256": "", + "analysis_sha256": "", + "endpoint_pairing_sha256": "", + "sensor_configuration_sha256": "", + "witness_report_sha256": "", + "privacy_review_sha256": "", + "security_review_sha256": "", + "guardrail_report_sha256": "", + "csi_capture_manifest_sha256": "", + "csi_sensor_identity_sha256": "", + "csi_calibration_sha256": "", + "synthetic_frames": 0, + "replay_frames": 0, + "paired_sequences": "", + "csi_source_count": "", + "offered_optical_frame_count": "", + "lidar_only": { + "sequence_count": "", + "accepted_sensor_frame_count": "", + "accepted_update_count": "", + "evaluable_duration_s": "", + "update_hz": "", + "position_error_sum_m": "", + "position_error_sample_count": "", + "position_error_m": "", + "lost_track_duration_s": "", + "evaluable_track_duration_s": "", + "lost_track_rate": "" + }, + "csi_only": { + "sequence_count": "", + "accepted_sensor_frame_count": "", + "accepted_update_count": "", + "evaluable_duration_s": "", + "update_hz": "", + "lost_track_duration_s": "", + "evaluable_track_duration_s": "", + "lost_track_rate": "" + }, + "fused": { + "sequence_count": "", + "accepted_sensor_frame_count": "", + "accepted_update_count": "", + "evaluable_duration_s": "", + "update_hz": "", + "position_error_sum_m": "", + "position_error_sample_count": "", + "position_error_m": "", + "lost_track_duration_s": "", + "evaluable_track_duration_s": "", + "lost_track_rate": "" + }, + "confidence": { + "bootstrap_resamples": "", + "bootstrap_seed_list_sha256": "", + "familywise_confidence_level": 0.975, + "position_error_reduction_lower": "", + "position_error_reduction_upper": "", + "lost_track_reduction_lower": "", + "lost_track_reduction_upper": "" + }, + "guardrails": { + "empty_false_track_rate": "", + "empty_false_track_rate_max": "", + "fused_p95_latency_ms": "", + "fused_p95_latency_max_ms": "", + "fused_frame_loss_rate": "<(offered-fused-accepted)/offered>", + "fused_frame_loss_rate_max": "", + "exclusion_fraction": "", + "exclusion_fraction_max": "", + "fused_update_rate_ratio_min": "", + "nonwinning_position_error_regression_max": "", + "nonwinning_lost_track_regression_max": "" + } +} +``` + +Run the hard arithmetic/provenance gate only after witness review: + +```bash +cd harness/ruview +node bin/cli.js nlos verify --repo ../.. \ + --evidence-file evidence/nlos/acceptance.json \ + --require-research-pass +``` + +Do not commit a fabricated placeholder file merely to exercise this command; +unit tests already cover synthetic fixtures. + +## 14. Reporting language + +Permitted after software only: + +> `SYNTHETIC`: the Rust, Swift, and web contracts pass deterministic replay and +> boundary tests. No live NLOS accuracy or iPhone hardware claim was evaluated. + +Permitted after a passing L2 capture: + +> `MEASURED` on capture `` under protocol `ruview-consumer-nlos-v1`, the +> external `/` LiDAR-only arm emitted `` accepted updates/s. Fusion +> reduced `` by `` relative to LiDAR-only over `` paired +> controlled sequences. This is L2 controlled-laboratory evidence for the named +> target/geometry, not built-in iPhone, diffuse-human, field, or safety evidence. + +Forbidden: + +1. “iPhone sees around corners” from ARKit depth, native/web build or external + track display; +2. “30 fps” from requested sensor/display rate rather than accepted end-to-end + live tracks; +3. “25 percent more accurate” when only lost-track rate passed; +4. “hardware validated” without sensor/firmware/capture/ground-truth witness; +5. pooled human/general-scene claims from the retroreflective target stratum; or +6. identity, intent, through-wall optical, collision-avoidance or safety claims. + +## 15. Stop and rollback criteria + +Stop capture immediately for consent/indicator failure, eye/electrical hazard, +credential compromise, raw-data leak, unapproved person entry, cross-tenant +join, provenance ambiguity, calibration/clock failure or confirmed high/critical +security issue. Quarantine the affected capture and never repair its label. + +Stop promotion when H1 fails, neither H2 endpoint passes, fusion guardrails are +materially worse, exclusions exceed the frozen tolerance, confidence analysis +cannot be reproduced, or any required artifact is absent. Roll back to +LiDAR-only, explicit replay, or unavailable. Issue a new protocol version before +recapture; do not tune against and reuse the opened confirmatory set. + +## 16. Primary sources + +1. Somasundaram et al., [Nature paper](https://doi.org/10.1038/s41586-026-10502-x). +2. [Author manuscript and MAS/particle-filter methods](https://arxiv.org/html/2605.17865v1). +3. [MIT project and reported real-time demonstration](https://cornar.media.mit.edu/). +4. [Upstream implementation and hardware procedure](https://github.com/sidsoma/consumer-nlos). +5. STMicroelectronics, [VL53L8CH histogram interface](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html). +6. STMicroelectronics, [P-NUCLEO-53L8A1](https://www.st.com/en/evaluation-tools/p-nucleo-53l8a1.html). diff --git a/docs/schemas/ruview-nlos-track-v1.schema.json b/docs/schemas/ruview-nlos-track-v1.schema.json new file mode 100644 index 00000000..2d35bc8d --- /dev/null +++ b/docs/schemas/ruview-nlos-track-v1.schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ruview.ai/schemas/ruview-nlos-track-v1.schema.json", + "title": "RuView NLOS track envelope v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", "sessionId", "sequence", "capturedAtUnixMs", "expiresAtUnixMs", + "source", "evidenceLevel", "algorithmVersion", "calibrationHash", "provenance", "tracks" + ], + "properties": { + "schema": { "const": "ruview.nlos.track.v1" }, + "sessionId": { "$ref": "#/$defs/label" }, + "sequence": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + "capturedAtUnixMs": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + "expiresAtUnixMs": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + "source": { "enum": ["live", "replay", "synthetic"] }, + "evidenceLevel": { "enum": ["l0_synthetic", "l1_measured", "l2_calibrated"] }, + "algorithmVersion": { "$ref": "#/$defs/label" }, + "calibrationHash": { "$ref": "#/$defs/sha256" }, + "provenance": { "$ref": "#/$defs/provenance" }, + "tracks": { + "type": "array", + "maxItems": 16, + "items": { "$ref": "#/$defs/track" } + } + }, + "allOf": [ + { + "if": { "properties": { "source": { "const": "synthetic" } }, "required": ["source"] }, + "then": { + "properties": { + "evidenceLevel": { "const": "l0_synthetic" }, + "calibrationHash": { "const": "0000000000000000000000000000000000000000000000000000000000000000" }, + "provenance": { + "properties": { + "transientKind": { "const": "replay" }, + "transport": { "const": "replay" } + } + } + } + } + }, + { + "if": { "properties": { "source": { "const": "live" } }, "required": ["source"] }, + "then": { + "properties": { + "evidenceLevel": { "enum": ["l1_measured", "l2_calibrated"] }, + "provenance": { + "properties": { + "transientKind": { "enum": ["raw_histogram", "compact_normalized_histogram"] }, + "histogramPreserved": { "const": true }, + "transport": { "enum": ["usb_serial", "ruview_server"] } + } + } + } + } + }, + { + "if": { "properties": { "source": { "const": "replay" } }, "required": ["source"] }, + "then": { + "properties": { + "provenance": { + "properties": { + "transientKind": { "const": "replay" }, + "histogramPreserved": { "const": true }, + "transport": { "const": "replay" } + } + } + } + } + }, + { + "if": { + "properties": { "evidenceLevel": { "const": "l2_calibrated" } }, + "required": ["evidenceLevel"] + }, + "then": { + "properties": { + "calibrationHash": { + "not": { "const": "0000000000000000000000000000000000000000000000000000000000000000" } + } + } + } + } + ], + "$defs": { + "label": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_.:-]+$" }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "unit": { "type": "number", "minimum": 0, "maximum": 1 }, + "vector": { + "type": "object", + "additionalProperties": false, + "required": ["x", "y", "z"], + "properties": { "x": { "type": "number" }, "y": { "type": "number" }, "z": { "type": "number" } } + }, + "positionVector": { + "type": "object", "additionalProperties": false, "required": ["x", "y", "z"], + "properties": { + "x": { "type": "number", "minimum": -100, "maximum": 100 }, + "y": { "type": "number", "minimum": -100, "maximum": 100 }, + "z": { "type": "number", "minimum": -100, "maximum": 100 } + } + }, + "velocityVector": { + "type": "object", "additionalProperties": false, "required": ["x", "y", "z"], + "properties": { + "x": { "type": "number", "minimum": -20, "maximum": 20 }, + "y": { "type": "number", "minimum": -20, "maximum": 20 }, + "z": { "type": "number", "minimum": -20, "maximum": 20 } + } + }, + "covarianceVector": { + "type": "object", "additionalProperties": false, "required": ["x", "y", "z"], + "properties": { + "x": { "type": "number", "minimum": 0, "maximum": 10 }, + "y": { "type": "number", "minimum": 0, "maximum": 10 }, + "z": { "type": "number", "minimum": 0, "maximum": 10 } + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["sensorId", "sensorModel", "firmwareVersion", "transientKind", "histogramPreserved", "transport"], + "properties": { + "sensorId": { "$ref": "#/$defs/label" }, + "sensorModel": { "$ref": "#/$defs/label" }, + "firmwareVersion": { "$ref": "#/$defs/label" }, + "transientKind": { "enum": ["raw_histogram", "compact_normalized_histogram", "depth_only", "replay"] }, + "histogramPreserved": { "type": "boolean" }, + "transport": { "enum": ["usb_serial", "ruview_server", "replay"] } + } + }, + "track": { + "type": "object", + "additionalProperties": false, + "required": [ + "trackId", "state", "positionM", "velocityMps", "covarianceDiagonalM2", + "confidence", "posteriorEntropy", "signalQuality", "modalityContributions" + ], + "properties": { + "trackId": { "$ref": "#/$defs/label" }, + "state": { "enum": ["tracking", "degraded", "unknown"] }, + "positionM": { "$ref": "#/$defs/positionVector" }, + "velocityMps": { "$ref": "#/$defs/velocityVector" }, + "covarianceDiagonalM2": { "$ref": "#/$defs/covarianceVector" }, + "confidence": { "$ref": "#/$defs/unit" }, + "posteriorEntropy": { "type": "number", "minimum": 0 }, + "signalQuality": { "$ref": "#/$defs/unit" }, + "modalityContributions": { + "type": "object", + "additionalProperties": false, + "required": ["lidar", "csi"], + "properties": { "lidar": { "$ref": "#/$defs/unit" }, "csi": { "$ref": "#/$defs/unit" } } + } + } + } + } +} diff --git a/docs/schemas/ruview-nlos-transient-v1.schema.json b/docs/schemas/ruview-nlos-transient-v1.schema.json new file mode 100644 index 00000000..0a9af927 --- /dev/null +++ b/docs/schemas/ruview-nlos-transient-v1.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ruview.ai/schemas/ruview-nlos-transient-v1.schema.json", + "title": "RuView raw optical transient frame v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", "sessionId", "sequence", "capturedAtUnixMs", "monotonicNs", "source", + "evidenceLevel", "binWidthPs", "startBin", "sensorPose", "calibrationHash", "provenance", "zones" + ], + "properties": { + "schema": { "const": "ruview.nlos.transient.v1" }, + "sessionId": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_.:-]+$" }, + "sequence": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + "capturedAtUnixMs": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, + "monotonicNs": { "type": "integer", "minimum": 0 }, + "source": { "enum": ["live", "replay", "synthetic"] }, + "evidenceLevel": { "enum": ["l0_synthetic", "l1_measured", "l2_calibrated"] }, + "binWidthPs": { "type": "number", "minimum": 1, "maximum": 10000 }, + "startBin": { "type": "integer", "minimum": 0, "maximum": 65535 }, + "sensorPose": { + "type": "object", + "additionalProperties": false, + "required": ["translationM", "quaternionXyzw"], + "properties": { + "translationM": { "$ref": "#/$defs/vector" }, + "quaternionXyzw": { + "type": "array", "minItems": 4, "maxItems": 4, + "items": { "type": "number", "minimum": -1, "maximum": 1 } + } + } + }, + "calibrationHash": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["sensorId", "sensorModel", "firmwareVersion", "transientKind", "histogramPreserved", "transport"], + "properties": { + "sensorId": { "$ref": "#/$defs/label" }, + "sensorModel": { "$ref": "#/$defs/label" }, + "firmwareVersion": { "$ref": "#/$defs/label" }, + "transientKind": { "enum": ["raw_histogram", "compact_normalized_histogram", "depth_only", "replay"] }, + "histogramPreserved": { "type": "boolean" }, + "transport": { "enum": ["usb_serial", "ruview_server", "replay"] } + } + }, + "zones": { + "type": "array", "minItems": 1, "maxItems": 64, + "items": { + "type": "object", "additionalProperties": false, + "required": ["zoneId", "wallPointM", "distanceM", "ambient", "histogram"], + "properties": { + "zoneId": { "type": "integer", "minimum": 0, "maximum": 63 }, + "wallPointM": { "$ref": "#/$defs/vector" }, + "distanceM": { "type": "number", "minimum": 0.01, "maximum": 10 }, + "ambient": { "type": "integer", "minimum": 0, "maximum": 4294967295 }, + "histogram": { + "type": "array", "minItems": 8, "maxItems": 128, + "items": { "type": "integer", "minimum": 0, "maximum": 65535 } + } + } + } + } + }, + "allOf": [ + { + "if": { "properties": { "source": { "const": "synthetic" } }, "required": ["source"] }, + "then": { + "properties": { + "evidenceLevel": { "const": "l0_synthetic" }, + "calibrationHash": { "const": "0000000000000000000000000000000000000000000000000000000000000000" }, + "provenance": { + "properties": { + "transientKind": { "const": "replay" }, + "transport": { "const": "replay" } + } + } + } + } + }, + { + "if": { "properties": { "source": { "const": "live" } }, "required": ["source"] }, + "then": { + "properties": { + "evidenceLevel": { "enum": ["l1_measured", "l2_calibrated"] }, + "provenance": { + "properties": { + "transientKind": { "enum": ["raw_histogram", "compact_normalized_histogram"] }, + "histogramPreserved": { "const": true }, + "transport": { "enum": ["usb_serial", "ruview_server"] } + } + } + } + } + }, + { + "if": { "properties": { "source": { "const": "replay" } }, "required": ["source"] }, + "then": { + "properties": { + "provenance": { + "properties": { + "transientKind": { "const": "replay" }, + "histogramPreserved": { "const": true }, + "transport": { "const": "replay" } + } + } + } + } + }, + { + "if": { + "properties": { "evidenceLevel": { "const": "l2_calibrated" } }, + "required": ["evidenceLevel"] + }, + "then": { + "properties": { + "calibrationHash": { + "not": { "const": "0000000000000000000000000000000000000000000000000000000000000000" } + } + } + } + } + ], + "$defs": { + "label": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_.:-]+$" }, + "vector": { + "type": "object", "additionalProperties": false, + "required": ["x", "y", "z"], + "properties": { "x": { "type": "number" }, "y": { "type": "number" }, "z": { "type": "number" } } + } + } +} diff --git a/docs/security/consumer-nlos-threat-model.md b/docs/security/consumer-nlos-threat-model.md new file mode 100644 index 00000000..b427a119 --- /dev/null +++ b/docs/security/consumer-nlos-threat-model.md @@ -0,0 +1,320 @@ +# Consumer NLOS threat model + +**Status:** Required design and release gate for ADR-328 through ADR-331 +**Last reviewed:** 2026-08-22 +**Scope:** enrolled histogram-capable ST transient acquisition with recorded +board/silicon/firmware/API identity, MAS tracking/reconstruction, CSI +fusion, RuVector/RuField/WorldGraph state, native iOS, web iOS, evidence and +contributor harness +**Security posture:** Research capability, local-first, identity-free, +fail-closed, no direct actuation + +## 1. Executive security decision + +Around-the-corner tracking changes the privacy boundary of a space: a person can +be observed without being in direct view of the operator or device. The output +is sparse probability/geometry rather than a photograph, but location, +trajectory, occupancy and joined RF evidence remain sensitive. “Camera-free” +must never be used as “privacy-free.” + +The first RuView deployment is a controlled, consented research experiment with +an external, upstream-compatible ST histogram sensor over local USB. Raw transients, CSI and +ground truth remain local under an approved retention plan. Live APIs expose +only bounded, expiring, session-scoped hypotheses. No NLOS output directly +controls an actuator or certifies that a hidden region is safe. + +This PR is software stage G0 only. Its local USB source label (`st-local`) is +not cryptographic sensor enrollment, and `ruview.nlos.track.v1` has no tenant, +workspace or world-frame field. The controls below that depend on those +bindings are mandatory live-promotion work; their documentation does not enable +L2 or make the current scaffold physically authenticated. + +Release is blocked by a confirmed high or critical finding, missing consent, +unauthenticated sensor/transport, ambiguous live/replay provenance, unbounded +parser, cross-tenant/world-frame join, or absent stale/replay protection. + +## 2. System and trust boundaries + +```mermaid +flowchart TD + A["Physical scene + ground truth"] --> B["ST sensor + firmware"] + B --> C["USB acquisition + transient parser"] + D["Authenticated CSI sensors"] --> E["Calibration + temporal join"] + C --> F["MAS likelihood + tracker"] + F --> E + E --> G["RuField / RuVector / WorldGraph"] + G --> H["Authenticated API + short-lived ticket"] + H --> I["Native and web iOS clients"] +``` + +Trust boundaries are crossed at every arrow. Physical proximity does not imply +logical trust. USB is a device/input boundary; the ST firmware and upstream +Python are supply-chain inputs; CSI is an independent, potentially malicious +modality; memory is tenant/purpose separated; API clients are untrusted; and +the UI is not evidence authority. + +The contributor harness reads repository metadata and optional bounded evidence +JSON. It does not receive raw captures, credentials, sensing authority, or a +runtime role. + +## 3. Assets + +| Asset | Security/privacy need | Default handling | +|---|---|---| +| Raw per-zone photon histograms | integrity, confidentiality, purpose/retention | local encrypted research store; never Git/logs | +| Raw CSI/CIR and RF calibration | confidentiality, tenant isolation, provenance | edge/local; no default cloud export | +| Ground-truth video/tags/trajectories | highest participant privacy and synchronization integrity | separated encrypted store, least access, scheduled deletion | +| Sensor identity and firmware/config digest | authenticity and chain of custody | certificate/key reference plus content digest; no private key export | +| Wall/background/extrinsic calibration | integrity, freshness, deployment binding | signed/content-addressed, expiring, invalidatable | +| Track posterior/covariance/history | confidentiality, freshness, non-reidentification | session ID, TTL, bounded history, purpose-limited | +| WorldGraph/RuVector state | tenant isolation, deletion, no identity escalation | tenant/workspace namespace, TTL, audited access | +| API credentials/tickets | confidentiality, replay resistance, least authority | native pairing secret in ThisDeviceOnly Keychain; web uses short-lived scoped ticket; never URL/log/local storage bearer | +| Capture/evidence manifest | integrity, reproducibility, nonrepudiation | canonical digest and witness record; no embedded raw data | +| Model/canonical response | integrity and license/provenance | pinned digest, reviewed source, immutable artifact | +| Availability/quality state | integrity and fail-closed semantics | explicit unavailable/degraded/unknown/expired states | + +## 4. Adversaries and misuse + +1. **Unauthorized operator** deploys or leaves sensing active without notice or + beyond the approved room/time/purpose. +2. **Nearby attacker** injects RF, optical, physical-motion or relay-surface + changes to produce, hide or move a track. +3. **Compromised sensor/firmware** fabricates histograms, sequence/timestamps, + calibration identity or firmware version. +4. **Compromised CSI node** contributes an extreme likelihood or replays a frame + to create false persistence. +5. **Malicious client** sends parser bombs, requests another tenant/session, + reuses a web ticket, scrapes track history or alters provenance labels. +6. **Supply-chain attacker** compromises upstream Git/Python/firmware, Cargo/npm/ + Swift packages, build tools or model artifacts. +7. **Insider/researcher** copies raw captures, joins session tracks across time, + bypasses deletion or selectively reports favorable trials. +8. **Curious contributor agent/harness** attempts to read secrets/raw data, + execute untrusted evidence, mutate code/hardware or promote a claim. +9. **Accidental failure** includes clock/coordinate mismatch, stale calibration, + overflow, NaN, queue buildup, app suspension and simulator/replay confusion. + +Out of scope does not mean acceptable: nation-state hardware implants and +physical destruction are not fully mitigated by this software, so high-assurance +or safety deployments require a separate hardware/security case. + +## 5. Security invariants + +1. `LIVE_HARDWARE`, `REPLAY`, and `SYNTHETIC` are mutually exclusive by type. + Unknown is never live. +2. A frame/track is usable only when schema, identity, session, sequence, + freshness, calibration, coordinate frame, numeric bounds and size pass. +3. Fused output requires two independently valid modalities within the frozen + time/space join bounds. Missing or rejected input cannot be labeled fused. +4. RuVector/WorldGraph similarity and persistence never upgrade acquisition + provenance, infer identity, or bypass freshness. +5. Raw sensor and ground-truth data is local/minimized by default and never + enters Git, harness manifests, package tarballs, logs or client telemetry. +6. Authorization is tenant/workspace/session/purpose scoped. A read scope is not + actuation authority. +7. Stale, contradictory, out-of-distribution or low-quality evidence produces + `unknown`/degraded, not a confident fallback. +8. No parser allocates from unchecked zone × bin × object × history dimensions. +9. No client rendering rate, simulator build or replay can satisfy the live + research gate. +10. A NLOS hypothesis has no direct actuator callback. + +## 6. Threat register + +Ratings describe the uncontrolled design. “Required control” is a release gate; +it is not a claim that every future implementation is automatically safe. + +| ID | Threat | Initial risk | Required controls | Verification / residual risk | +|---|---|---:|---|---| +| T01 | Covert or overbroad sensing beyond direct view | Critical | explicit approved purpose/space/time, participant notice/consent, persistent indicator, pause/stop, background off, audit and deletion | privacy review plus physical walkthrough; residual High for misuse by an authorized operator, so no unconsented deployment | +| T02 | Unauthenticated sensor or CSI impersonation | Critical | ADR-305 identity, enrollment, tenant/session binding, TLS where networked, digest/certificate on frame/calibration | forged/unknown identity tests; residual Medium for stolen keys, mitigated by rotation/revocation | +| T03 | Replay/duplicate/future timestamp creates false track | High | monotonic sequence, nonce/session, bounded skew/TTL, duplicate cache, clock uncertainty, stale clear | exact/changed replay, wrap and clock-jump tests; residual Low/Medium under clock loss → unknown | +| T04 | Calibration/background substitution or poisoning | High | signed/content-addressed calibration bound to sensor/config/room, expiry/OOD, controlled empty capture, immutable audit | mismatched/expired/poisoned calibration tests; residual Medium for slow physical drift | +| T05 | Direct line of sight contaminates “hidden” result | High | opaque geometry check, independent scene inspection, registered camera/ground-truth exclusion, capture manifest | randomized occluder/negative trials; residual Medium for unnoticed reflections/view gaps | +| T06 | Optical/RF adversarial injection or physical spoof | High | per-modality quality/OOD, bounded influence, optical-only/CSI-only ablation, multi-view/temporal consistency, unknown on contradiction | extreme likelihood and conflicting-modality tests; residual High in adversarial environments, so no safety claim | +| T07 | Coordinate or time-frame mismatch fuses different targets | High | typed units/frame IDs, calibrated transform+uncertainty, bounded pairing skew, no cross-frame fallback | incompatible frame/unit/skew test matrix; residual Low after fail-closed controls | +| T08 | Parser overflow, NaN/Inf, oversized histogram/track | High | pre-allocation dimension/product bounds, finite checks, fixed max message/evidence size, fuzz/property tests, bounded tails | sanitizers/fuzz/boundary corpus; residual Low/Medium for third-party decoders | +| T09 | Queue/memory/CPU exhaustion hides freshness | High | bounded queues/particles/history, newest-frame policy, rate limits, timeouts, backpressure/drop counters | overload/slow-client tests; residual Medium under sustained physical/authorized load | +| T10 | Web ticket/token theft or cross-origin stream | High | ATS/TLS, short-lived one-use scoped ticket, fixed origin, no bearer in URL/log/local storage, CSP, reconnect bounds | expiry/reuse/origin/tenant negative integration tests; residual Medium for compromised client device | +| T11 | Native/web stale UI remains visually live | High | expiry timer independent of incoming messages, clear on suspend/disconnect/decode error, provenance/quality always visible | fake-clock/suspend/disconnect/replay UI tests; residual Low | +| T12 | Cross-tenant or cross-session memory leakage | Critical | typed namespace at ingest/join/store/query, authorization, TTL/deletion, no global person ID | tenant/session isolation tests and access audit; residual Medium for admin/backup controls | +| T13 | Re-identification from trajectory/embedding | High | random session track IDs, no identity training/labels, bounded TTL, purpose limitation, aggregation, access logging | privacy review and deletion tests; residual Medium/High in small populations, so identity use prohibited | +| T14 | Raw data, secrets or positions leak via Git/log/telemetry/package | High | repo incident controls, `.gitignore`, secret/data scans, redaction, synthetic fixtures, no raw telemetry, tarball review | repository-policy, package dry run, log tests; residual Low/Medium for human export | +| T15 | Upstream/firmware/dependency/model compromise | High | pin commits/versions/digests, license/source review, isolated sidecar, dependency audit, signed release/provenance, no auto-flash | SBOM/audit/reproducible hash and firmware review; residual Medium for build toolchain | +| T16 | Malicious evidence JSON/path escape/code execution | High | regular bounded repository-confined JSON, realpath containment, no eval/import, strict schema/digest/arithmetic | traversal/symlink/oversize/malformed tests; residual Low | +| T17 | Selective reporting, leakage or metric gaming | High | preregistration, immutable grouped split, external ground truth, paired arms, all strata/secondary metrics, witness review | independent analysis reproduction; residual Medium for undisclosed captures | +| T18 | Learned RF model copies LiDAR labels and appears independent | High | group splits, test on external ground truth, LiDAR/CSI/fused ablations, teacher quality masks, sealed holdout | leakage/adjoining-frame tests; residual Medium under environmental confounding | +| T19 | Harness/agent mutation gains sensing or release authority | High | MCP/static tools read-only and default-deny; opt-in CLI builds only in a trusted checkout with scrubbed environment/redacted tails; MetaHarness dev-only; human promotion | policy/tool/build-output tests and package manifest; residual Medium because build tools execute repository code | +| T20 | Track drives unsafe actuator/decision | Critical | hypothesis-only API, explicit no actuator, ADR-321/327 independent governed action and deployment safety case | interface/source tests; residual Critical if bypassed, therefore release blocker | +| T21 | Private/undocumented Apple API use | High | public SDK only, `canImport`/availability checks, official-doc review, no hidden entitlement/reverse engineering | Xcode source/entitlement review; residual Low with external-sensor-only claim | +| T22 | Eye/laser or electrical hazard from modified hardware | High | unmodified Class 1 sensor, manufacturer limits, approved power/enclosure, no emitter modification, trained operator | hardware checklist; residual Medium; any optical modification requires new safety review | + +## 7. Input-boundary controls + +### 7.1 Transient frame + +Reject before allocating or mutating state when any of these is true: + +1. schema/version is unknown; +2. zone/bin dimensions are zero, exceed configured maxima, or their product + overflows; +3. bin width, timestamps, pose, intrinsics, wall points, ambient level or + normalized counts are non-finite/out of physical configured range; +4. sensor/session identity is absent, revoked or mismatched; +5. sequence is duplicate/regressing or time is stale/future beyond uncertainty; +6. firmware/config/calibration digest differs from the session manifest; +7. transform is non-invertible, units differ, or coordinate frame is unknown; +8. calibration is not `VALID`; or +9. the source-provenance transition is illegal. + +### 7.2 Track/API message + +`ruview.nlos.track.v1` validation happens before store/render. Bound message +bytes, track count, history length, covariance/particle summary, strings and +metadata. Position/velocity/covariance are finite and covariance is valid under +the chosen representation. The client owns an independent expiry timer. +Tenant/workspace/world-frame, capture-certificate and audience/origin checks are +the required L2 contract. Current v1 validates session/schema/sequence/freshness, +bounded exact shape, calibration hash and generic transient provenance only; it +must remain G0 until the missing authorization/frame bindings are versioned end +to end. + +### 7.3 Evidence record + +The harness permits at most 1 MiB, regular JSON inside the canonical repository +root. `realpath` containment rejects `..` and symlink escapes. JSON is parsed as +data, never imported or executed. Research pass requires exact live provenance, +external ground truth, frozen protocol, digests, zero synthetic/replay frames, +minimum sample size, valid rates/errors and the frozen arithmetic. + +## 8. Authentication, authorization and key handling + +1. Before live promotion, sensor enrollment creates a non-secret, scoped sensor identity reference and protects + the private key outside captures/manifests. Revocation invalidates future + frames and live capability certificates. +2. Service authorization is least-privilege: read tracks for one tenant, + workspace, session and purpose. It grants no calibration write, firmware + flash, memory export, evidence promotion or actuation. +3. The current G0 browser exchanges an authenticated session for a 30-second, + one-use WebSocket ticket bound to the server session; the client requires a + server-session acknowledgement, pins the returned WSS URL to the configured + same authority, and the server uses exact CORS. It does **not** yet bind + tenant/workspace/audience/origin claims in the ticket. Those bindings and + their negative tests are mandatory before L2. Tickets are not stored + persistently. Native pairing tokens are scoped, revocable, validated before + use, and stored as `WhenUnlockedThisDeviceOnly` Keychain data. +4. TLS certificate verification is on in release builds. Debug/local exceptions + are explicit, non-exportable release configuration. +5. Secrets are never command-line arguments when avoidable and never printed in + error tails. Harness tools accept no raw token/API key field. +6. Research-store encryption keys are purpose/experiment scoped, kept outside + captures, manifests and backups, access-audited, rotated on compromise, and + destroyed at retention expiry. Backup retention cannot silently defeat + deletion or crypto-erasure evidence. + +## 9. Privacy impact and data lifecycle + +| Stage | Minimization | Access/retention | Deletion proof | +|---|---|---|---| +| Capture | record only approved zones/modalities/window; no audio; ground truth separated | named researchers, encrypted local store, frozen short schedule | manifest tombstone plus storage audit | +| Calibration | no person present; bind to room/sensor/config | operators and pipeline; expire on change | invalidation record and artifact deletion | +| Inference | process raw at edge; emit bounded position/covariance/provenance | live authorized clients only | TTL and session teardown tests | +| RuVector/WorldGraph | no stable identity; session scope; minimal embeddings/relations | tenant/purpose-scoped queries | namespace purge and index compaction evidence | +| Evidence | hashes and aggregate metrics, no raw person data | reviewers | retain per research governance without reconstructing raw capture | +| Logs/telemetry | counters, digests truncated where needed, redacted errors | operators/security | rotation verification | + +Consent/notice must explain around-corner sensing in plain language. An optical +sensor without RGB imagery can still infer a hidden person's location and +movement. Withdrawal/deletion limitations for already aggregated, non-personal +published metrics are documented before participation. + +## 10. Availability and safe degradation + +Every dependency has an explicit safe state: + +| Failure | Required behavior | +|---|---| +| sensor disconnect or malformed frame | stop live optical updates; emit unavailable/unknown | +| calibration/OOD failure | invalidate likelihood; request recalibration; no cached live fallback | +| CSI missing | optical-only label if optical remains valid; never fused | +| optical missing | RF-only/coarse label if separately permitted; never NLOS/fused | +| clock/coordinate disagreement | no join; record diagnostic counter | +| tracker numeric collapse | reset bounded filter and emit unknown during reacquisition | +| API auth/ticket expiry | disconnect and clear live state | +| native suspension/web background | expire display independently of server | +| RuVector/WorldGraph unavailable | current local track may continue without persistence; no memory fabrication | +| harness/MetaHarness unavailable | no runtime impact; run direct crate/app tests | + +## 11. Security verification plan + +Minimum software evidence is staged: current G0 must pass the checks applicable +to its implemented schema; every listed check is mandatory before live L2 +promotion. A test cannot stand in for a field the protocol does not yet carry. + +1. Rust unit/property/fuzz or boundary tests for transient/track decode, + dimension-product overflow, numeric extremes, calibration/provenance state, + temporal/coordinate join and bounded filter behavior. +2. Swift and TypeScript cross-language golden vectors plus malformed/stale/ + oversized/provenance tests. +3. Authentication integration tests for invalid/expired/reused/cross-origin/ + cross-tenant tickets and disconnect state. +4. Empty/rejected modality, conflicting modality, replay, clock jump, sequence + wrap, overload and slow-client tests. +5. Repository secret/data incident scan, Cargo/npm/Swift dependency review, + license/SBOM review and `npm pack --dry-run` inspection. +6. Harness policy, evidence traversal/oversize/synthetic rejection, manifest, + brain/flywheel replay and full legacy tests. +7. Manual review for private Apple API/entitlement use, raw data/credentials in + fixtures/logs, direct actuator callbacks and overclaimed metrics. + +### Closed dependency exception record `NLOS-SEC-EX-001` + +| Field | Record | +|---|---| +| Status | **CLOSED BY REMEDIATION**; no risk exception was approved or consumed | +| Original finding | high-severity `image-size@1.2.1` advisories [GHSA-w3rx-r6r6-pgpr](https://github.com/advisories/GHSA-w3rx-r6r6-pgpr) and [GHSA-5p2g-fcmc-qvqq](https://github.com/advisories/GHSA-5p2g-fcmc-qvqq) through the prior Metro lock | +| Remediation | aligned the Expo SDK 55 dependency set and regenerated `ui/mobile/package-lock.json`; the resulting 2026-08-22 audit reports zero high and zero critical findings | +| Verification | `npm ci --ignore-scripts` plus `npm audit --json`; local audit digest `b6e5236f2b07dec4f714d60e7c8a5683348464455392d636fc77b9c591bb8dcb` | +| Remaining findings | ten moderate transitive build-tool findings, including `uuid` through `xcode`; tracked normally and not covered by an exception | +| CI policy | fail every high or critical dependency finding; no NLOS allowlist exists | +| Promotion mapping | the former dependency block is removed; all other G0→G1 physical, privacy, authorization, provenance and witness gates remain closed until independently satisfied | + +The closed record is retained so reviewers can see why the lockfile changed and +verify that the project remediated rather than silently accepted the finding. + +Live L2 evidence additionally needs a physical setup inspection, firmware and +sensor identity witness, direct-line-of-sight exclusion, external ground-truth +clock test, capture access/retention review, and independent reproduction of the +analysis from the immutable manifest. + +## 12. Incident response and rollback + +On suspected unauthorized sensing, provenance failure, key theft, raw-data leak, +metric manipulation or unsafe downstream use: + +1. stop capture and live stream; revoke sensor/client credentials and tickets; +2. freeze bounded logs/manifests without copying unnecessary raw participant + data; +3. disable the NLOS/fusion capability certificate and UI live flag; +4. notify the privacy/security owner and affected participants/organizations as + required; +5. determine affected tenants/sessions/captures/models and delete/quarantine per + policy; +6. patch and add a focused regression test; rerun the complete gate; +7. re-enable only after independent review and, where evidence was affected, a + fresh preregistered capture. + +Rollback may return to optical-only, RF-only, replay-only or fully unavailable. +It never changes old provenance or represents replay as a live substitute. + +## 13. Residual-risk decision + +Even with controls, authorized misuse, physical spoofing, environment shift, +re-identification from trajectories and supply-chain compromise retain material +risk. Therefore the accepted scope is controlled, consented RuView Labs +research and advisory visualization. Public-space surveillance, covert sensing, +biometric identity, through-wall safety guarantees, vehicle collision avoidance, +medical monitoring and autonomous actuation are not approved by these ADRs. + +Any expansion requires a new threat model, evidence level, deployment/privacy +review, operational monitoring, incident/rollback plan and accountable owner. diff --git a/harness/ruview/.claude/skills/consumer-nlos/SKILL.md b/harness/ruview/.claude/skills/consumer-nlos/SKILL.md new file mode 100644 index 00000000..7ad91283 --- /dev/null +++ b/harness/ruview/.claude/skills/consumer-nlos/SKILL.md @@ -0,0 +1,164 @@ +--- +name: consumer-nlos +description: Plan, review, and verify RuView consumer time-of-flight non-line-of-sight research, including raw-transient reproduction, motion-induced aperture, CSI fusion, iOS boundaries, and real-hardware acceptance evidence. +--- + +# Consumer NLOS research and verification + +Use this skill to plan or review RuView's consumer time-of-flight non-line-of-sight +research. It is an advisory contributor workflow. It does not capture hardware, +authorize sensing, replace the Rust runtime, or turn a simulator result into a +hardware claim. + +## Non-negotiable measurement boundary + +The MIT method consumes a transient photon-count histogram for each SPAD zone. +Do not substitute an ARKit scene-depth map, mesh, conventional point cloud, +WiFi CSI frame, or synthetic replay. Those inputs may provide pose, context, +RF likelihoods, or deterministic software tests, but they do not contain the +delayed optical multipath samples needed for NLOS inversion. + +The supported reproduction target is a pinned, upstream-compatible ST assembly +with verified raw or compact-normalized histogram access. Record the actual +board/silicon/firmware/API identity rather than inferring it from the kit name. +Pin the full upstream `sidsoma/consumer-nlos` commit, scoped enrollment +reference, firmware/configuration, calibration, capture manifests, analysis and +RuView commit before comparing results. + +## Start here + +```bash +npx @ruvnet/ruview nlos plan +npx @ruvnet/ruview nlos verify --repo . +``` + +The first command returns the four gated phases. The second performs static +inspection only. `STATIC_DISCOVERY_ONLY`, an available-build pass, or a skip is +not full Gate A, a research result, or proof of live NLOS behavior. + +Run available build gates explicitly: + +```bash +npx @ruvnet/ruview nlos verify --repo . --run-builds +``` + +Repository selection and build execution are CLI-only. The MCP tools remain +read-only advisory surfaces and reject `repo` and `run_builds` so a remote tool +call cannot choose an arbitrary checkout or launch its toolchains. +Run builds only in a trusted checkout: Cargo, Swift, Jest and Expo may execute +repository code. The verifier scrubs the child environment and redacts output, +but it is not a sandbox. + +Expected optional surfaces: + +1. `v2/crates/ruview-nlos` for typed transient frames, motion-induced aperture + state, calibrated likelihoods, fusion, and `ruview.nlos.track.v1` output. +2. `ui/ios-nlos` for the pure Swift contract plus the public-API ARKit adapter. +3. `ui/mobile` for authenticated live tracks and deterministic `SYNTHETIC` + replay. The browser does not claim direct access to iPhone photon histograms. + +An absent optional surface is an explicit skip. Once its marker exists, missing +required files or a mismatched schema is a failure. + +## Upstream reproduction + +1. Freeze the experiment protocol before capture. Record hypotheses, endpoints, + exclusions, randomization, scene strata, sensor configuration, target class, + calibration procedure, and statistical analysis. +2. Reproduce the upstream tracker before changing its state model. Use verified + live transient histograms, external ground truth, an opaque line-of-sight blocker, + and a controlled relay surface. Start with the upstream retroreflective + target before diffuse targets. +3. Preserve zone, timing bin width, counts, ambient signal, sensor timestamp, + monotonic sequence, wall geometry, pose, calibration identity, and capture + provenance. Never flatten a transient frame to a depth point before NLOS + preprocessing. +4. Measure end-to-end track update rate. The preregistered reproduction gate is + at least 27 Hz, meaning within ten percent of the reported 30 Hz target. +5. Treat published accuracy as `CLAIMED` until reproduced. Generated fixtures + are `SYNTHETIC`; captured replay remains distinct `REPLAY`/L1 evidence. Use + `MEASURED` only with a named live capture manifest, L2 level and external + ground-truth reproducer. + +## CSI fusion experiment + +Use paired sequences: the same capture split, initial state, target trajectory, +and scoring code for LiDAR-only and LiDAR-plus-CSI arms. Keep calibration and +threshold selection inside the training/calibration partition. The test +partition remains sealed until LiDAR-only, CSI-only, and fused arms are frozen. + +The retained fusion model must satisfy both conditions: + +1. The LiDAR-only reproduction arm produces at least 27 end-to-end updates per + second on live hardware. +2. Fusion reduces mean target position error by at least 25 percent **or** + reduces lost-track rate by at least 25 percent relative to LiDAR-only over at + least 100 paired sequences. + +Do not count a replay, simulator, duplicated frame, or LiDAR-derived pseudo-CSI +feature as independent RF evidence. Report both endpoints even when only one is +the success endpoint. Stratify by reflectivity, range, motion, relay surface, +and RF geometry so a pooled gain cannot hide a failing domain. + +## Evidence gate + +Create a repository-contained JSON record conforming to +`ruview.nlos.acceptance.v1`. It separates `LIVE_HARDWARE` provenance, +`MEASURED` claim tag and L2 evidence, and requires external ground truth, frozen +protocol, actual sensor/transient identity, nonzero artifact/review digests, +zero synthetic/replay frames, independent CSI with CSI-only ablation, at least +100 paired sequences, full paired position-endpoint coverage, shared lost-track +exposure, recomputable aggregate metrics (including offered rate and fused frame +loss), adjusted confidence intervals and frozen numeric guardrails. The v1 live +gate accepts an enrolled external ST VL53L8-series sensor label; it cannot be +used to attest an iPhone sensor. + +Then require the real-hardware gate: + +```bash +npx @ruvnet/ruview nlos verify --repo . \ + --evidence-file evidence/nlos/acceptance.json \ + --require-research-pass +``` + +The verifier fails closed on malformed, out-of-repository, oversized, replay, +or synthetic evidence. A passing JSON record is an integrity and arithmetic +check, not an independent audit of the physical capture; reviewers must inspect +the immutable capture manifest and ground-truth synchronization evidence. + +## Native and web iOS boundary + +Apple's documented ARKit surfaces expose processed scene depth, smoothed depth, +pose, and reconstructed meshes. Until Apple documents access to the required +per-zone transient histograms, use those APIs only for line-of-sight context, +pose, display, or transport. A native app may consume authenticated, versioned RuView tracks from +the external sensor pipeline. A web client may consume an authenticated live +stream or a visibly watermarked replay. Neither is evidence that the built-in +iPhone LiDAR ran the MIT inversion. + +## Privacy and security + +1. Record approved purpose, space, time, controller and retention. Obtain all + required operator/participant notice and consent, provide pause/withdrawal, + show a persistent indicator, and prohibit hidden sensing. +2. Before any live promotion, bind every frame to an authenticated sensor, + session, calibration, tenant/workspace, coordinate frame and monotonic + sequence. The G0 scaffold does not yet provide every binding. Reject stale, + duplicate, future, oversized, or coordinate-frame-incompatible inputs before + fusion. +3. Keep raw transients and CSI local by default. Export bounded track hypotheses, + covariance, provenance, and expiration unless the frozen protocol requires + raw capture retention under an approved data policy. +4. Never infer identity. Use random, session-scoped track identifiers and short + expiry. Unknown quality stays unknown; it never falls back to a live claim. +5. RuVector, RuField, and WorldGraph carry state and evidence. They do not create + sensing authority or compensate for missing photons. + +## Authoritative references + +1. [Nature paper](https://doi.org/10.1038/s41586-026-10502-x) +2. [Author manuscript](https://arxiv.org/html/2605.17865v1) +3. [MIT project and reported 30 Hz demonstration](https://cornar.media.mit.edu/) +4. [Upstream implementation](https://github.com/sidsoma/consumer-nlos) +5. [ST VL53L8CH histogram interface](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html) +6. [Apple scene-depth sample](https://developer.apple.com/documentation/arkit/displaying-a-point-cloud-using-scene-depth) diff --git a/harness/ruview/.harness/claims.json b/harness/ruview/.harness/claims.json index 0bc9e6a9..ba1731a7 100644 --- a/harness/ruview/.harness/claims.json +++ b/harness/ruview/.harness/claims.json @@ -8,6 +8,8 @@ "ruview_verify", "ruview_node_monitor", "ruview_guidance", + "ruview_nlos_plan", + "ruview_nlos_verify", "ruview_memory_search" ], "grants": { diff --git a/harness/ruview/.harness/manifest.json b/harness/ruview/.harness/manifest.json index ece03ea4..80c4ff0f 100644 --- a/harness/ruview/.harness/manifest.json +++ b/harness/ruview/.harness/manifest.json @@ -13,17 +13,18 @@ ".claude/settings.json": "57d03e8995363bd120fb6d515702967afd0bd557797051301ff8f8156c845824", ".claude/skills/calibrate-room/SKILL.md": "4b29c7c331f47acad3c0f51b3d3d8f5b5573e316e081bae71dbe21a47fa95240", ".claude/skills/cognitum-spaces/SKILL.md": "96ae42cc72ad31dbb2f34d59e874c4d15f2e55fc969cd1f610dc1b9a4138840e", + ".claude/skills/consumer-nlos/SKILL.md": "de0c1efc1fa8b9ab143b8a9abcffe5f21974b05635f5f4b5b2066cb738e4c042", ".claude/skills/onboard/SKILL.md": "97ee71f0aa985cfc03bb8e764789bb55c4f9fd5dae10a116c1071eab85b5893f", ".claude/skills/provision-node/SKILL.md": "5f73823794ed5f0b25c102aa8b1bf2dd534a1ec468173d8330c2af0ca24f239c", ".claude/skills/train-pose/SKILL.md": "92aebd4423470eb10eabaee642ec3493284d98b7ae9785e0f34378c709746e65", ".claude/skills/verify/SKILL.md": "2d38d240e9810a7827e2ebd3717dc0f85c646cc92e46c3812fe77c5b9eb40b76", - ".harness/claims.json": "9544cee8012328eb26856a9fff38d80a73f09e48a2da7537f6c3695521b0fd54", - ".harness/mcp-policy.json": "749e9f24bde85921a45b91bf6fa4ab5605675af769c04c53fe69129019662d3e", + ".harness/claims.json": "7a3810b11e874bcd19330d82dab121dfb6980a106335187b34acca9838c0117d", + ".harness/mcp-policy.json": "a40583341c86e4f609593c7db3cbb2d6522185c7dfae715a242dc04a3709fc16", ".mcp/servers.json": "fec6075400f8350d8075beac8306690355c4b015425bfd0e5f52966234e9d66f", - "CLAUDE.md": "46d5514f4cbf4d94f683f76aa6d50a3dce2ec5a95fd87b9154ed4752f5ea0e16", + "CLAUDE.md": "73b3858f601251d7c6d2527d1eaf2befca841bf80e615280ddbf46d924f692f6", "LICENSE": "631f94984f626818d42ecf717aa6e8e0afd4f9f355ca706bd2effafbd1416d06", - "README.md": "ce716f07b4b93d5b86285a46cc7be1c6ff48d95ee12ac73518dbf2fb7b61d82e", - "bin/cli.js": "0c96bf65a189732a35760c88a3d441a5bd6ce53abbd3bfaa73141665825e1be1", + "README.md": "205d816eb03a577a5768490af8de23ba7214db7e6e2aae333c3faa2e633b72fd", + "bin/cli.js": "df3033d1f369af819c1db6498d06c6b331ee9394608491a12b3cedb45b746266", "brain/corpus/core.jsonl": "c0fb7b079ded157059b91601361429944697dae3cc42abc00dfe1a680986b0f4", "flywheel/evaluations.json": "ac4ff1f897a2444870cd2b8ae8aee8b1578e61467aeca4db57893f41be98a572", "flywheel/fixture.mjs": "de71be88753d0da4695d91011b54380c994a018986fafba36cb13739307a9bce", @@ -31,12 +32,13 @@ "flywheel/genome.json": "75db44a3cab70d9459fc8c07863f640ac1214bfaa243483939e1506d63f51214", "flywheel/replay.mjs": "0670ca0b03701f4afe0b4bca8a3d58d481676b61a94a5b98c6a425aefb1159ab", "flywheel/run.mjs": "6d4f97db16900c45367b6538848cbe1915af999e663720dfc51f2bb1698f1cd0", - "package.json": "5d29ef238f310c9ee5c57501ab651acc0f856f831b696ada187de71e4b5935a6", + "package.json": "3f3c518dcfc2d678484736471aa033b1078b8950cfa9bb66d9e72e5e9d3bf022", "scripts/sync-skills.mjs": "43715dab61e204dc91bbd61755810e8fdb2f66e2b0c0bd791b4bf48a2e293565", "scripts/update-manifest.mjs": "8f56764b8f70aed55da0c7e2417ae875b0d58d781d839b6db7f115f08af61e6b", "scripts/verify-manifest.mjs": "6491a221762efcfeb3e749ecab243b204f17fd5bc871f3d4025597f31b8f0f10", "skills/calibrate-room.md": "4b29c7c331f47acad3c0f51b3d3d8f5b5573e316e081bae71dbe21a47fa95240", "skills/cognitum-spaces.md": "96ae42cc72ad31dbb2f34d59e874c4d15f2e55fc969cd1f610dc1b9a4138840e", + "skills/consumer-nlos.md": "de0c1efc1fa8b9ab143b8a9abcffe5f21974b05635f5f4b5b2066cb738e4c042", "skills/onboard.md": "97ee71f0aa985cfc03bb8e764789bb55c4f9fd5dae10a116c1071eab85b5893f", "skills/provision-node.md": "5f73823794ed5f0b25c102aa8b1bf2dd534a1ec468173d8330c2af0ca24f239c", "skills/train-pose.md": "92aebd4423470eb10eabaee642ec3493284d98b7ae9785e0f34378c709746e65", @@ -48,14 +50,15 @@ "src/hosts/codex.js": "d41ecd132ce2db7b47aad9cebbc020d70e6810d48c3554858d099ff2e8f6608b", "src/hosts/index.js": "ab276c41ab722bcdf72c2d1649cecbb760ae05c41c1372aae4c2447aa7c11539", "src/mcp-server.js": "8b2ee4b939b25c1b1f507b295a43a2ebad852b8bac9d31af9bf7fb39b181c12e", - "src/policy.js": "169cc33793b91ee01a78e6403aeefff1ab5e92f33b73eb85912fe03666464975", + "src/nlos.js": "7582c1984ac092c51df81f1339e50a662eddbdf961d3ef25d51d95d96f33b824", + "src/policy.js": "9ddc08231c1d1c84412a2bea03219ca77972a6254ccda940b9ce26df88e7a7bc", "src/process-runner.js": "49533b038044dfb8bc76ed01c030d06a9856ead0836157fb693e2a7d40f786d6", "src/redact.js": "ebf1afff46341078706b0401838c53db043603586e280d51ece5cf1feba35189", "src/repo-trust.js": "06e2a94d7113ed936f208a12b7fcc785801c215a3e2c5e7418f6238d991a289c", "src/spaces.js": "45ef786537cb2a446db5e926e5a1c10b73639d2767dec84611f914f78d4325eb", - "src/tools.js": "55960c9a677661763e0317fd54ccc787c2edb39c87371c7fbc40cd55f0761c04" + "src/tools.js": "3bb10444cd420006b7d9ba74617d1e6627528bd042e102592690311852998611" }, - "filesDigest": "28a3bbd9bbcf966df9fae8ec6ea5be3441f6ab535c1636bb1ce33f67b827d423", + "filesDigest": "44b60d20bafd4b51ef2a550e3437cb3a87f0774be60f46f284f588aaf1210e21", "brainDigest": "c0fb7b079ded157059b91601361429944697dae3cc42abc00dfe1a680986b0f4", "gateFingerprint": "6e53c784eee38310188948fc75fb49e6b4ebc04e247d01b903fa8c8a92d67bdd", "developmentPins": { diff --git a/harness/ruview/.harness/manifest.sha256 b/harness/ruview/.harness/manifest.sha256 index bbc2afef..25525046 100644 --- a/harness/ruview/.harness/manifest.sha256 +++ b/harness/ruview/.harness/manifest.sha256 @@ -1 +1 @@ -478ccaff9aa249bc7ea6e20551ccc9ac88697a3cc91a7b55400337c5e939a19e manifest.json +ee03d67e99978163b6181cb9849c2e111722edc528bbc0083b090e7ce1612080 manifest.json diff --git a/harness/ruview/.harness/mcp-policy.json b/harness/ruview/.harness/mcp-policy.json index 857cd7f5..e11c3e3b 100644 --- a/harness/ruview/.harness/mcp-policy.json +++ b/harness/ruview/.harness/mcp-policy.json @@ -12,6 +12,8 @@ "ruview_verify", "ruview_node_monitor", "ruview_guidance", + "ruview_nlos_plan", + "ruview_nlos_verify", "ruview_memory_search" ], "guardedReadTools": { diff --git a/harness/ruview/CLAUDE.md b/harness/ruview/CLAUDE.md index d65d8927..82d9d404 100644 --- a/harness/ruview/CLAUDE.md +++ b/harness/ruview/CLAUDE.md @@ -19,6 +19,7 @@ accuracy number: `ruview_onboard`, `ruview_claim_check`, `ruview_verify`, `ruview_node_monitor`, `ruview_calibrate`, `ruview_node_flash`, `ruview_guidance`, +`ruview_nlos_plan`, `ruview_nlos_verify`, `ruview_spaces_list`, `ruview_memory_search`. Start unfamiliar work with `ruview_guidance`; its capability status, source paths, validation commands, and limitations are @@ -35,9 +36,14 @@ bound. It grants no write or action authority. ## Skills -`onboard` · `provision-node` · `calibrate-room` · `train-pose` · `verify` · `cognitum-spaces` +`onboard` · `provision-node` · `calibrate-room` · `train-pose` · `verify` · `cognitum-spaces` · `consumer-nlos` (`npx @ruvnet/ruview skill `). +The NLOS tools are advisory repository/research gates. They never capture a +sensor or turn `SYNTHETIC` replay/build evidence into a live-hardware claim. +Consumer NLOS requires per-zone photon timing histograms; ARKit depth and CSI +are context/fusion inputs, not substitutes for that measurement. + ## Don'ts - Don't present WiFi sensing as camera-grade. diff --git a/harness/ruview/README.md b/harness/ruview/README.md index 18b9a694..4c998c60 100644 --- a/harness/ruview/README.md +++ b/harness/ruview/README.md @@ -17,6 +17,8 @@ npx @ruvnet/ruview claim-check --file REPORT.md # the honesty guardrail (non-z npx @ruvnet/ruview verify # run the deterministic proof (VERDICT: PASS) npx @ruvnet/ruview doctor # self-check (tools, adapters, local CLIs) npx @ruvnet/ruview guidance --topic homecore --query "Wasmtime plugins" +npx @ruvnet/ruview nlos plan +npx @ruvnet/ruview nlos verify --repo . npx @ruvnet/ruview spaces --resource spaces npx @ruvnet/ruview spaces --resource events --limit 25 npx @ruvnet/ruview --help @@ -40,12 +42,52 @@ Exposed both as CLI verbs and as an MCP server (`npx @ruvnet/ruview mcp start`): | `ruview_calibrate` | ADR-151 room pipeline (baseline→enroll→train-room→room-watch) | | `ruview_node_flash` | Build+flash firmware (Windows/ESP-IDF; mutating, guarded) | | `ruview_guidance` | Source-cited code map, capability maturity, validation commands, and limitations | +| `ruview_nlos_plan` | Advisory, phase-gated consumer transient-LiDAR and CSI fusion plan | +| `ruview_nlos_verify` | Static/build inspection plus fail-closed live-hardware evidence arithmetic | | `ruview_spaces_list` | OAuth-only paging for sites/buildings/floors/spaces/zones/entities/events/alerts (guarded over MCP) | | `ruview_memory_search` | Search the reviewed, source-cited contributor brain | Every tool is **fail-closed**: missing repo / python / binary / port → an honest negative, never a fabricated success. +### Consumer NLOS research gate + +`nlos plan` and `nlos verify` are optional contributor aids for ADR-328 through +ADR-331. They do not capture sensors and are not required by the RuView runtime. +The verifier discovers `v2/crates/ruview-nlos`, `ui/ios-nlos`, and the NLOS +surface in `ui/mobile`. An absent optional surface is reported as `ABSENT`; a +partial, escaping, oversized, or marker-incompatible surface fails shallow +discovery. Contract tests remain authoritative. Add `--run-builds` to +run the build/test commands supported by the current host. Xcode and hardware +remain explicit skips when unavailable. + +Repository selection and build execution are local-CLI capabilities. The MCP +tool auto-detects its checkout, performs read-only inspection/evidence checks, +and rejects `repo` or `run_builds` arguments. Run CLI builds only in a trusted +checkout: the environment is allowlisted and tails redacted, but Cargo/Swift/ +Jest/Expo execution is not a sandbox. + +The research gate accepts only a preregistered `LIVE_HARDWARE` record with an +external ground-truth capture manifest. It requires the LiDAR-only arm to run at +least 27 Hz and CSI fusion to reduce mean position error or lost-track rate by +at least 25 percent over 100 or more paired sequences, with adjusted confidence, +paired aggregate arithmetic, independent CSI ablation, frozen guardrails, and +witness/privacy/security report digests. `SYNTHETIC` and replay +frames can validate software, but can never pass that gate: + +```bash +npx @ruvnet/ruview nlos verify --repo . --run-builds +npx @ruvnet/ruview nlos verify --repo . \ + --evidence-file evidence/nlos/acceptance.json \ + --require-research-pass +``` + +The boundary is deliberate: the MIT method needs per-zone photon-arrival +histograms. Apple currently documents processed ARKit scene depth and mesh, so +the native adapter uses those for pose/context only; the web adapter consumes +authenticated tracks or visibly labelled deterministic replay. Neither surface +claims direct iPhone NLOS reconstruction. + ### Cognitum Spaces OAuth Activate the additional read scope through the Rust CLI, then use the same @@ -100,7 +142,7 @@ as evidence. ## Skills Host-neutral playbooks in `skills/` (`onboard`, `provision-node`, `calibrate-room`, -`train-pose`, `verify`, `cognitum-spaces`). `npx @ruvnet/ruview skill ` +`train-pose`, `verify`, `cognitum-spaces`, `consumer-nlos`). `npx @ruvnet/ruview skill ` prints one. ## Use as a Claude Code MCP server diff --git a/harness/ruview/bin/cli.js b/harness/ruview/bin/cli.js index 04bb6b98..2f6508e2 100644 --- a/harness/ruview/bin/cli.js +++ b/harness/ruview/bin/cli.js @@ -72,6 +72,9 @@ Operator tools: flash --port COM8 --variant s3-8mb [--confirm] build+flash firmware (Windows/ESP-IDF) guidance [--topic homecore] [--query "Wasmtime"] source-cited code/capability map spaces [--resource sites|...|alerts] [--limit 50] page OAuth-bound Cognitum spatial resources + nlos plan print the staged consumer-NLOS research plan + nlos verify [--repo ] [--run-builds] inspect optional NLOS surfaces and run available gates + [--evidence-file ] [--require-research-pass] Harness: doctor verify tools, adapters, and local CLI discovery @@ -155,6 +158,23 @@ export async function run(args) { } console.error('Usage: ruview mcp start'); return 2; } + case 'nlos': { + const action = rest[0] || 'plan'; + if (action === 'plan') { + const result = await runTool('ruview_nlos_plan', {}, { source: 'cli' }); + pjson(result); return result.ok ? 0 : 1; + } + if (action === 'verify') { + const toolArgs = {}; + if (flags.repo !== undefined) toolArgs.repo = String(flags.repo); + if (flags['evidence-file'] !== undefined) toolArgs.evidence_file = String(flags['evidence-file']); + if (flags['run-builds'] === true) toolArgs.run_builds = true; + if (flags['require-research-pass'] === true) toolArgs.require_research_pass = true; + const result = await runTool('ruview_nlos_verify', toolArgs, { source: 'cli' }); + pjson(result); return result.ok ? 0 : 1; + } + console.error('Usage: ruview nlos plan|verify'); return 2; + } case 'agent': { if (rest[0] !== 'run') { console.error('Usage: ruview agent run --host claude-code|codex --prompt "..." [--repo ]'); return 2; } const hostName = String(flags.host || 'codex'); diff --git a/harness/ruview/package.json b/harness/ruview/package.json index 75938192..15e3de45 100644 --- a/harness/ruview/package.json +++ b/harness/ruview/package.json @@ -11,6 +11,7 @@ "./guardrails": "./src/guardrails.js", "./brain": "./src/brain.js", "./guidance": "./src/guidance.js", + "./nlos": "./src/nlos.js", "./hosts": "./src/hosts/index.js" }, "files": [ @@ -47,6 +48,8 @@ "ruview", "csi", "channel-state-information", + "consumer-lidar", + "non-line-of-sight", "pose-estimation", "presence-detection", "esp32", diff --git a/harness/ruview/skills/consumer-nlos.md b/harness/ruview/skills/consumer-nlos.md new file mode 100644 index 00000000..7ad91283 --- /dev/null +++ b/harness/ruview/skills/consumer-nlos.md @@ -0,0 +1,164 @@ +--- +name: consumer-nlos +description: Plan, review, and verify RuView consumer time-of-flight non-line-of-sight research, including raw-transient reproduction, motion-induced aperture, CSI fusion, iOS boundaries, and real-hardware acceptance evidence. +--- + +# Consumer NLOS research and verification + +Use this skill to plan or review RuView's consumer time-of-flight non-line-of-sight +research. It is an advisory contributor workflow. It does not capture hardware, +authorize sensing, replace the Rust runtime, or turn a simulator result into a +hardware claim. + +## Non-negotiable measurement boundary + +The MIT method consumes a transient photon-count histogram for each SPAD zone. +Do not substitute an ARKit scene-depth map, mesh, conventional point cloud, +WiFi CSI frame, or synthetic replay. Those inputs may provide pose, context, +RF likelihoods, or deterministic software tests, but they do not contain the +delayed optical multipath samples needed for NLOS inversion. + +The supported reproduction target is a pinned, upstream-compatible ST assembly +with verified raw or compact-normalized histogram access. Record the actual +board/silicon/firmware/API identity rather than inferring it from the kit name. +Pin the full upstream `sidsoma/consumer-nlos` commit, scoped enrollment +reference, firmware/configuration, calibration, capture manifests, analysis and +RuView commit before comparing results. + +## Start here + +```bash +npx @ruvnet/ruview nlos plan +npx @ruvnet/ruview nlos verify --repo . +``` + +The first command returns the four gated phases. The second performs static +inspection only. `STATIC_DISCOVERY_ONLY`, an available-build pass, or a skip is +not full Gate A, a research result, or proof of live NLOS behavior. + +Run available build gates explicitly: + +```bash +npx @ruvnet/ruview nlos verify --repo . --run-builds +``` + +Repository selection and build execution are CLI-only. The MCP tools remain +read-only advisory surfaces and reject `repo` and `run_builds` so a remote tool +call cannot choose an arbitrary checkout or launch its toolchains. +Run builds only in a trusted checkout: Cargo, Swift, Jest and Expo may execute +repository code. The verifier scrubs the child environment and redacts output, +but it is not a sandbox. + +Expected optional surfaces: + +1. `v2/crates/ruview-nlos` for typed transient frames, motion-induced aperture + state, calibrated likelihoods, fusion, and `ruview.nlos.track.v1` output. +2. `ui/ios-nlos` for the pure Swift contract plus the public-API ARKit adapter. +3. `ui/mobile` for authenticated live tracks and deterministic `SYNTHETIC` + replay. The browser does not claim direct access to iPhone photon histograms. + +An absent optional surface is an explicit skip. Once its marker exists, missing +required files or a mismatched schema is a failure. + +## Upstream reproduction + +1. Freeze the experiment protocol before capture. Record hypotheses, endpoints, + exclusions, randomization, scene strata, sensor configuration, target class, + calibration procedure, and statistical analysis. +2. Reproduce the upstream tracker before changing its state model. Use verified + live transient histograms, external ground truth, an opaque line-of-sight blocker, + and a controlled relay surface. Start with the upstream retroreflective + target before diffuse targets. +3. Preserve zone, timing bin width, counts, ambient signal, sensor timestamp, + monotonic sequence, wall geometry, pose, calibration identity, and capture + provenance. Never flatten a transient frame to a depth point before NLOS + preprocessing. +4. Measure end-to-end track update rate. The preregistered reproduction gate is + at least 27 Hz, meaning within ten percent of the reported 30 Hz target. +5. Treat published accuracy as `CLAIMED` until reproduced. Generated fixtures + are `SYNTHETIC`; captured replay remains distinct `REPLAY`/L1 evidence. Use + `MEASURED` only with a named live capture manifest, L2 level and external + ground-truth reproducer. + +## CSI fusion experiment + +Use paired sequences: the same capture split, initial state, target trajectory, +and scoring code for LiDAR-only and LiDAR-plus-CSI arms. Keep calibration and +threshold selection inside the training/calibration partition. The test +partition remains sealed until LiDAR-only, CSI-only, and fused arms are frozen. + +The retained fusion model must satisfy both conditions: + +1. The LiDAR-only reproduction arm produces at least 27 end-to-end updates per + second on live hardware. +2. Fusion reduces mean target position error by at least 25 percent **or** + reduces lost-track rate by at least 25 percent relative to LiDAR-only over at + least 100 paired sequences. + +Do not count a replay, simulator, duplicated frame, or LiDAR-derived pseudo-CSI +feature as independent RF evidence. Report both endpoints even when only one is +the success endpoint. Stratify by reflectivity, range, motion, relay surface, +and RF geometry so a pooled gain cannot hide a failing domain. + +## Evidence gate + +Create a repository-contained JSON record conforming to +`ruview.nlos.acceptance.v1`. It separates `LIVE_HARDWARE` provenance, +`MEASURED` claim tag and L2 evidence, and requires external ground truth, frozen +protocol, actual sensor/transient identity, nonzero artifact/review digests, +zero synthetic/replay frames, independent CSI with CSI-only ablation, at least +100 paired sequences, full paired position-endpoint coverage, shared lost-track +exposure, recomputable aggregate metrics (including offered rate and fused frame +loss), adjusted confidence intervals and frozen numeric guardrails. The v1 live +gate accepts an enrolled external ST VL53L8-series sensor label; it cannot be +used to attest an iPhone sensor. + +Then require the real-hardware gate: + +```bash +npx @ruvnet/ruview nlos verify --repo . \ + --evidence-file evidence/nlos/acceptance.json \ + --require-research-pass +``` + +The verifier fails closed on malformed, out-of-repository, oversized, replay, +or synthetic evidence. A passing JSON record is an integrity and arithmetic +check, not an independent audit of the physical capture; reviewers must inspect +the immutable capture manifest and ground-truth synchronization evidence. + +## Native and web iOS boundary + +Apple's documented ARKit surfaces expose processed scene depth, smoothed depth, +pose, and reconstructed meshes. Until Apple documents access to the required +per-zone transient histograms, use those APIs only for line-of-sight context, +pose, display, or transport. A native app may consume authenticated, versioned RuView tracks from +the external sensor pipeline. A web client may consume an authenticated live +stream or a visibly watermarked replay. Neither is evidence that the built-in +iPhone LiDAR ran the MIT inversion. + +## Privacy and security + +1. Record approved purpose, space, time, controller and retention. Obtain all + required operator/participant notice and consent, provide pause/withdrawal, + show a persistent indicator, and prohibit hidden sensing. +2. Before any live promotion, bind every frame to an authenticated sensor, + session, calibration, tenant/workspace, coordinate frame and monotonic + sequence. The G0 scaffold does not yet provide every binding. Reject stale, + duplicate, future, oversized, or coordinate-frame-incompatible inputs before + fusion. +3. Keep raw transients and CSI local by default. Export bounded track hypotheses, + covariance, provenance, and expiration unless the frozen protocol requires + raw capture retention under an approved data policy. +4. Never infer identity. Use random, session-scoped track identifiers and short + expiry. Unknown quality stays unknown; it never falls back to a live claim. +5. RuVector, RuField, and WorldGraph carry state and evidence. They do not create + sensing authority or compensate for missing photons. + +## Authoritative references + +1. [Nature paper](https://doi.org/10.1038/s41586-026-10502-x) +2. [Author manuscript](https://arxiv.org/html/2605.17865v1) +3. [MIT project and reported 30 Hz demonstration](https://cornar.media.mit.edu/) +4. [Upstream implementation](https://github.com/sidsoma/consumer-nlos) +5. [ST VL53L8CH histogram interface](https://www.st.com/en/imaging-and-photonics-solutions/vl53l8ch.html) +6. [Apple scene-depth sample](https://developer.apple.com/documentation/arkit/displaying-a-point-cloud-using-scene-depth) diff --git a/harness/ruview/src/nlos.js b/harness/ruview/src/nlos.js new file mode 100644 index 00000000..d0c12165 --- /dev/null +++ b/harness/ruview/src/nlos.js @@ -0,0 +1,743 @@ +// SPDX-License-Identifier: MIT +// Advisory consumer-NLOS planning and verification helpers. +// +// This module never captures a sensor, changes runtime state, or treats a build +// as sensing evidence. It is intentionally dependency-free so RuView remains +// fully usable when MetaHarness, Xcode, hardware, or optional UI surfaces are +// absent (ADR-331). + +import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'; +import { isAbsolute, join, relative, resolve, sep } from 'node:path'; +import { scrubEnvironment } from './process-runner.js'; +import { redact } from './redact.js'; + +export const NLOS_EVIDENCE_SCHEMA = 'ruview.nlos.acceptance.v1'; +export const NLOS_TRACK_SCHEMA = 'ruview.nlos.track.v1'; + +const MAX_EVIDENCE_BYTES = 1024 * 1024; +const MAX_SURFACE_FILE_BYTES = 4 * 1024 * 1024; +const SHA256 = /^[a-f0-9]{64}$/i; +const ZERO_SHA256 = /^0{64}$/; +const GIT_SHA1 = /^[a-f0-9]{40}$/i; +const ZERO_GIT_SHA1 = /^0{40}$/; +const SAFE_LABEL = /^[A-Za-z0-9._:+-]{1,128}$/; +const EVIDENCE_KEYS = new Set([ + 'schema', + 'source', + 'claim_tag', + 'evidence_level', + 'ground_truth', + 'protocol_frozen_before_capture', + 'witness_reviewed', + 'privacy_review_passed', + 'security_review_passed', + 'los_exclusion_verified', + 'independent_csi_verified', + 'sensor_model', + 'transient_kind', + 'sensor_configured_max_hz', + 'upstream_commit', + 'protocol_sha256', + 'capture_manifest_sha256', + 'sensor_identity_sha256', + 'calibration_sha256', + 'firmware_sha256', + 'analysis_sha256', + 'endpoint_pairing_sha256', + 'sensor_configuration_sha256', + 'witness_report_sha256', + 'privacy_review_sha256', + 'security_review_sha256', + 'guardrail_report_sha256', + 'csi_capture_manifest_sha256', + 'csi_sensor_identity_sha256', + 'csi_calibration_sha256', + 'synthetic_frames', + 'replay_frames', + 'paired_sequences', + 'csi_source_count', + 'offered_optical_frame_count', + 'lidar_only', + 'csi_only', + 'fused', + 'confidence', + 'guardrails', +]); +const ARM_KEYS = new Set([ + 'sequence_count', + 'accepted_sensor_frame_count', + 'accepted_update_count', + 'evaluable_duration_s', + 'update_hz', + 'position_error_sum_m', + 'position_error_sample_count', + 'position_error_m', + 'lost_track_duration_s', + 'evaluable_track_duration_s', + 'lost_track_rate', +]); +const CSI_ARM_KEYS = new Set([ + 'sequence_count', + 'accepted_sensor_frame_count', + 'accepted_update_count', + 'evaluable_duration_s', + 'update_hz', + 'lost_track_duration_s', + 'evaluable_track_duration_s', + 'lost_track_rate', +]); +const CONFIDENCE_KEYS = new Set([ + 'bootstrap_resamples', + 'bootstrap_seed_list_sha256', + 'familywise_confidence_level', + 'position_error_reduction_lower', + 'position_error_reduction_upper', + 'lost_track_reduction_lower', + 'lost_track_reduction_upper', +]); +const GUARDRAIL_KEYS = new Set([ + 'empty_false_track_rate', + 'empty_false_track_rate_max', + 'fused_p95_latency_ms', + 'fused_p95_latency_max_ms', + 'fused_frame_loss_rate', + 'fused_frame_loss_rate_max', + 'exclusion_fraction', + 'exclusion_fraction_max', + 'fused_update_rate_ratio_min', + 'nonwinning_position_error_regression_max', + 'nonwinning_lost_track_regression_max', +]); + +const SURFACES = Object.freeze([ + { + id: 'rust-core', + base: 'v2/crates/ruview-nlos', + marker: 'v2/crates/ruview-nlos/Cargo.toml', + required: [ + 'v2/crates/ruview-nlos/Cargo.toml', + 'v2/crates/ruview-nlos/src/lib.rs', + ], + }, + { + id: 'native-ios', + base: 'ui/ios-nlos', + marker: 'ui/ios-nlos/Package.swift', + required: [ + 'ui/ios-nlos/Package.swift', + 'ui/ios-nlos/RuViewNLOS.xcodeproj/project.pbxproj', + ], + }, + { + id: 'web-ios', + base: 'ui/mobile', + marker: 'ui/mobile/src/types/nlos.ts', + required: [ + 'ui/mobile/src/types/nlos.ts', + 'ui/mobile/src/services/nlos.service.ts', + 'ui/mobile/src/stores/nlosStore.ts', + 'ui/mobile/src/hooks/useNlosStream.ts', + 'ui/mobile/src/screens/NLOSScreen/index.tsx', + ], + }, +]); + +function fileContains(path, needle) { + try { return readFileSync(path, 'utf8').includes(needle); } catch { return false; } +} + +function inspectRepoFile(repo, path) { + const candidate = join(repo, path); + if (!existsSync(candidate)) return { ok: false, reason: 'missing', candidate }; + try { + const canonicalRepo = realpathSync(repo); + const canonicalFile = realpathSync(candidate); + const rel = relative(canonicalRepo, canonicalFile); + if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) { + return { ok: false, reason: 'outside-repo', candidate }; + } + const stat = statSync(canonicalFile); + if (!stat.isFile()) return { ok: false, reason: 'not-regular', candidate }; + if (stat.size > MAX_SURFACE_FILE_BYTES) return { ok: false, reason: 'oversize', candidate }; + return { ok: true, candidate: canonicalFile }; + } catch { + return { ok: false, reason: 'unreadable', candidate }; + } +} + +function inspectSurface(repo, surface) { + const marker = inspectRepoFile(repo, surface.marker); + if (!marker.ok && marker.reason === 'missing') { + return { id: surface.id, status: 'ABSENT', base: surface.base, findings: [] }; + } + const states = new Map(surface.required.map((path) => [path, inspectRepoFile(repo, path)])); + const findings = []; + if (!marker.ok) findings.push(`${surface.marker}:${marker.reason}`); + for (const [path, state] of states) { + if (!state.ok && path !== surface.marker) findings.push(`${path}:${state.reason}`); + } + + if (surface.id === 'rust-core' && !findings.length) { + const cargo = states.get('v2/crates/ruview-nlos/Cargo.toml').candidate; + const lib = states.get('v2/crates/ruview-nlos/src/lib.rs').candidate; + if (!fileContains(cargo, 'name = "ruview-nlos"')) findings.push('rust-core:crate-name-mismatch'); + if (!fileContains(lib, NLOS_TRACK_SCHEMA)) findings.push(`rust-core:${NLOS_TRACK_SCHEMA}:missing`); + } + if (surface.id === 'native-ios' && !findings.length) { + const packageFile = states.get('ui/ios-nlos/Package.swift').candidate; + if (!fileContains(packageFile, 'RuViewNLOSCore')) findings.push('native-ios:RuViewNLOSCore-product-missing'); + if (!fileContains(packageFile, 'RuViewNLOSApple')) findings.push('native-ios:RuViewNLOSApple-product-missing'); + } + if (surface.id === 'web-ios' && !findings.length) { + const typeFile = states.get('ui/mobile/src/types/nlos.ts').candidate; + if (!fileContains(typeFile, NLOS_TRACK_SCHEMA)) findings.push(`web-ios:${NLOS_TRACK_SCHEMA}:missing`); + } + + return { + id: surface.id, + status: findings.length ? 'MALFORMED' : 'PRESENT', + base: surface.base, + findings, + }; +} + +function buildResult(id, command, result) { + return { + id, + command: command.join(' '), + status: result.ok ? 'PASS' : 'FAIL', + exit: result.status, + error: redact(result.error, { env: process.env }), + stdout_tail: redact(result.stdout.slice(-1200), { env: process.env }), + stderr_tail: redact(result.stderr.slice(-800), { env: process.env }), + }; +} + +async function runBuilds(repo, surfaces, { runCommand, whichCommand }) { + const results = []; + const buildEnv = scrubEnvironment(process.env); + const present = new Set(surfaces.filter((item) => item.status === 'PRESENT').map((item) => item.id)); + + if (present.has('rust-core')) { + if (!whichCommand('cargo')) results.push({ id: 'rust-core', status: 'SKIPPED', reason: 'cargo_unavailable' }); + else { + const args = ['test', '--locked', '--offline', '-p', 'ruview-nlos']; + results.push(buildResult('rust-core', ['cargo', ...args], await runCommand('cargo', args, { + cwd: join(repo, 'v2'), timeout: 600000, env: buildEnv, + }))); + } + } + + if (present.has('native-ios')) { + if (!whichCommand('swift')) results.push({ id: 'native-swift', status: 'SKIPPED', reason: 'swift_unavailable' }); + else { + const args = ['test']; + results.push(buildResult('native-swift', ['swift', ...args], await runCommand('swift', args, { + cwd: join(repo, 'ui/ios-nlos'), timeout: 600000, env: buildEnv, + }))); + } + if (process.platform !== 'darwin' || !whichCommand('xcodebuild')) { + results.push({ id: 'native-xcode', status: 'SKIPPED', reason: 'xcodebuild_requires_macos' }); + } else { + const args = [ + '-project', 'RuViewNLOS.xcodeproj', '-scheme', 'RuViewNLOS', '-sdk', 'iphonesimulator', + '-destination', 'generic/platform=iOS Simulator', 'CODE_SIGNING_ALLOWED=NO', 'build', + ]; + results.push(buildResult('native-xcode', ['xcodebuild', ...args], await runCommand('xcodebuild', args, { + cwd: join(repo, 'ui/ios-nlos'), timeout: 900000, env: buildEnv, + }))); + } + } + + if (present.has('web-ios')) { + const mobile = join(repo, 'ui/mobile'); + if (!existsSync(join(mobile, 'node_modules'))) { + results.push({ id: 'web-ios', status: 'SKIPPED', reason: 'node_modules_absent_run_npm_ci' }); + } else { + const commands = [ + [process.execPath, [join(mobile, 'node_modules/jest/bin/jest.js'), '--runInBand'], 600000, 'web-test'], + [process.execPath, [join(mobile, 'node_modules/typescript/bin/tsc'), '--noEmit'], 600000, 'web-types'], + [process.execPath, [join(mobile, 'node_modules/eslint/bin/eslint.js'), '.'], 600000, 'web-lint'], + [process.execPath, [join(mobile, 'node_modules/expo/bin/cli'), 'export', '--platform', 'web'], 900000, 'web-export'], + ]; + for (const [cmd, args, timeout, id] of commands) { + if (!existsSync(args[0])) results.push({ id, status: 'SKIPPED', reason: 'local_tool_unavailable' }); + else { + const env = id === 'web-export' + ? { ...buildEnv, CI: '1', EXPO_NO_TELEMETRY: '1', EXPO_OFFLINE: '1' } + : buildEnv; + results.push(buildResult(id, ['node', ...args], await runCommand(cmd, args, { + cwd: mobile, timeout, env, + }))); + } + } + } + } + return results; +} + +function number(value, name, { min = 0, max = Infinity, positive = false } = {}) { + if (typeof value !== 'number' || !Number.isFinite(value)) return `${name}:must-be-finite-number`; + if (positive ? value <= min : value < min) return `${name}:below-minimum`; + if (value > max) return `${name}:above-maximum`; + return null; +} + +function nearlyEqual(actual, expected) { + return Math.abs(actual - expected) <= Math.max(1e-9, Math.abs(expected) * 1e-6); +} + +function isNonzeroSha256(value) { + const text = String(value || ''); + return SHA256.test(text) && !ZERO_SHA256.test(text); +} + +function validateArm(arm, name, pairedSequences, { position = true } = {}) { + const findings = []; + if (!arm || typeof arm !== 'object' || Array.isArray(arm)) return [`${name}:missing`]; + const allowedKeys = position ? ARM_KEYS : CSI_ARM_KEYS; + for (const key of Object.keys(arm)) { + if (!allowedKeys.has(key)) findings.push(`${name}.${key}:unknown-field`); + } + for (const key of allowedKeys) { + if (!(key in arm)) findings.push(`${name}.${key}:missing`); + } + const numericFindings = [ + number(arm.update_hz, `${name}.update_hz`, { positive: true, max: 240 }), + number(arm.lost_track_rate, `${name}.lost_track_rate`, { max: 1 }), + number(arm.evaluable_duration_s, `${name}.evaluable_duration_s`, { positive: true, max: 1e9 }), + number(arm.lost_track_duration_s, `${name}.lost_track_duration_s`, { max: 1e9 }), + number(arm.evaluable_track_duration_s, `${name}.evaluable_track_duration_s`, { positive: true, max: 1e9 }), + ]; + if (position) numericFindings.push( + number(arm.position_error_m, `${name}.position_error_m`, { max: 1000 }), + number(arm.position_error_sum_m, `${name}.position_error_sum_m`, { max: 1e12 }), + ); + for (const finding of numericFindings) if (finding) findings.push(finding); + for (const [key, min, max] of [ + ['sequence_count', 1, 1e6], + ['accepted_sensor_frame_count', 1, 1e9], + ['accepted_update_count', 1, 1e9], + ...(position ? [['position_error_sample_count', 1, 1e6]] : []), + ]) { + if (!Number.isSafeInteger(arm[key]) || arm[key] < min || arm[key] > max) { + findings.push(`${name}.${key}:invalid-safe-integer`); + } + } + if (Number.isSafeInteger(pairedSequences) && arm.sequence_count !== pairedSequences) { + findings.push(`${name}.sequence_count:must-equal-paired-sequences`); + } + if (Number.isSafeInteger(arm.accepted_update_count) + && Number.isSafeInteger(arm.accepted_sensor_frame_count) + && arm.accepted_update_count > arm.accepted_sensor_frame_count) { + findings.push(`${name}.accepted_update_count:above-sensor-frame-count`); + } + if (position && Number.isSafeInteger(arm.position_error_sample_count) + && Number.isSafeInteger(arm.accepted_update_count) + && arm.position_error_sample_count > arm.accepted_update_count) { + findings.push(`${name}.position_error_sample_count:above-update-count`); + } + if (position && Number.isSafeInteger(pairedSequences) + && Number.isSafeInteger(arm.position_error_sample_count) + && arm.position_error_sample_count !== pairedSequences) { + findings.push(`${name}.position_error_sample_count:must-equal-paired-sequences`); + } + if (Number.isFinite(arm.lost_track_duration_s) + && Number.isFinite(arm.evaluable_track_duration_s) + && arm.lost_track_duration_s > arm.evaluable_track_duration_s) { + findings.push(`${name}.lost_track_duration_s:above-evaluable-duration`); + } + if (Number.isSafeInteger(arm.accepted_update_count) && Number.isFinite(arm.evaluable_duration_s) + && arm.evaluable_duration_s > 0 && Number.isFinite(arm.update_hz) + && !nearlyEqual(arm.update_hz, arm.accepted_update_count / arm.evaluable_duration_s)) { + findings.push(`${name}.update_hz:arithmetic-mismatch`); + } + if (position && Number.isSafeInteger(arm.position_error_sample_count) && arm.position_error_sample_count > 0 + && Number.isFinite(arm.position_error_sum_m) && Number.isFinite(arm.position_error_m) + && !nearlyEqual(arm.position_error_m, arm.position_error_sum_m / arm.position_error_sample_count)) { + findings.push(`${name}.position_error_m:arithmetic-mismatch`); + } + if (Number.isFinite(arm.lost_track_duration_s) && Number.isFinite(arm.evaluable_track_duration_s) + && arm.evaluable_track_duration_s > 0 && Number.isFinite(arm.lost_track_rate) + && !nearlyEqual(arm.lost_track_rate, arm.lost_track_duration_s / arm.evaluable_track_duration_s)) { + findings.push(`${name}.lost_track_rate:arithmetic-mismatch`); + } + return findings; +} + +function validateConfidence(confidence) { + const findings = []; + if (!confidence || typeof confidence !== 'object' || Array.isArray(confidence)) return ['confidence:missing']; + for (const key of Object.keys(confidence)) { + if (!CONFIDENCE_KEYS.has(key)) findings.push(`confidence.${key}:unknown-field`); + } + for (const key of CONFIDENCE_KEYS) { + if (!(key in confidence)) findings.push(`confidence.${key}:missing`); + } + if (!Number.isSafeInteger(confidence.bootstrap_resamples) + || confidence.bootstrap_resamples < 10_000 || confidence.bootstrap_resamples > 10_000_000) { + findings.push('confidence.bootstrap_resamples:out-of-range'); + } + if (!isNonzeroSha256(confidence.bootstrap_seed_list_sha256)) { + findings.push('confidence.bootstrap_seed_list_sha256:invalid'); + } + for (const finding of [ + number(confidence.familywise_confidence_level, 'confidence.familywise_confidence_level', { min: 0.975, max: 1 }), + number(confidence.position_error_reduction_lower, 'confidence.position_error_reduction_lower', { min: -100, max: 1 }), + number(confidence.position_error_reduction_upper, 'confidence.position_error_reduction_upper', { min: -100, max: 1 }), + number(confidence.lost_track_reduction_lower, 'confidence.lost_track_reduction_lower', { min: -100, max: 1 }), + number(confidence.lost_track_reduction_upper, 'confidence.lost_track_reduction_upper', { min: -100, max: 1 }), + ]) if (finding) findings.push(finding); + if (Number.isFinite(confidence.position_error_reduction_lower) + && Number.isFinite(confidence.position_error_reduction_upper) + && confidence.position_error_reduction_lower > confidence.position_error_reduction_upper) { + findings.push('confidence.position_error_reduction:interval-inverted'); + } + if (Number.isFinite(confidence.lost_track_reduction_lower) + && Number.isFinite(confidence.lost_track_reduction_upper) + && confidence.lost_track_reduction_lower > confidence.lost_track_reduction_upper) { + findings.push('confidence.lost_track_reduction:interval-inverted'); + } + return findings; +} + +function validateGuardrails(guardrails) { + const findings = []; + if (!guardrails || typeof guardrails !== 'object' || Array.isArray(guardrails)) return ['guardrails:missing']; + for (const key of Object.keys(guardrails)) { + if (!GUARDRAIL_KEYS.has(key)) findings.push(`guardrails.${key}:unknown-field`); + } + for (const key of GUARDRAIL_KEYS) { + if (!(key in guardrails)) findings.push(`guardrails.${key}:missing`); + } + for (const finding of [ + number(guardrails.empty_false_track_rate, 'guardrails.empty_false_track_rate', { max: 1 }), + number(guardrails.empty_false_track_rate_max, 'guardrails.empty_false_track_rate_max', { max: 1 }), + number(guardrails.fused_p95_latency_ms, 'guardrails.fused_p95_latency_ms', { max: 60_000 }), + number(guardrails.fused_p95_latency_max_ms, 'guardrails.fused_p95_latency_max_ms', { positive: true, max: 60_000 }), + number(guardrails.fused_frame_loss_rate, 'guardrails.fused_frame_loss_rate', { max: 1 }), + number(guardrails.fused_frame_loss_rate_max, 'guardrails.fused_frame_loss_rate_max', { max: 1 }), + number(guardrails.exclusion_fraction, 'guardrails.exclusion_fraction', { max: 1 }), + number(guardrails.exclusion_fraction_max, 'guardrails.exclusion_fraction_max', { max: 1 }), + number(guardrails.fused_update_rate_ratio_min, 'guardrails.fused_update_rate_ratio_min', { max: 1 }), + number(guardrails.nonwinning_position_error_regression_max, 'guardrails.nonwinning_position_error_regression_max', { max: 10 }), + number(guardrails.nonwinning_lost_track_regression_max, 'guardrails.nonwinning_lost_track_regression_max', { max: 10 }), + ]) if (finding) findings.push(finding); + for (const [value, limit, label] of [ + ['empty_false_track_rate', 'empty_false_track_rate_max', 'empty_false_track_rate'], + ['fused_p95_latency_ms', 'fused_p95_latency_max_ms', 'fused_p95_latency_ms'], + ['fused_frame_loss_rate', 'fused_frame_loss_rate_max', 'fused_frame_loss_rate'], + ['exclusion_fraction', 'exclusion_fraction_max', 'exclusion_fraction'], + ]) { + if (Number.isFinite(guardrails[value]) && Number.isFinite(guardrails[limit]) + && guardrails[value] > guardrails[limit]) findings.push(`guardrails.${label}:above-frozen-limit`); + } + return findings; +} + +/** Validate a preregistered, real-hardware comparison. Never accepts replay-only evidence. */ +export function evaluateResearchEvidence(evidence) { + if (evidence === null || evidence === undefined) { + return { + status: 'NOT_EVALUATED', pass: false, + reason: 'no_evidence_file', + note: 'Software or replay validation is not real-hardware NLOS evidence.', + }; + } + const findings = []; + if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) findings.push('evidence:must-be-object'); + if (findings.length) return { status: 'FAIL', pass: false, findings }; + + for (const key of Object.keys(evidence)) { + if (!EVIDENCE_KEYS.has(key)) findings.push(`${key}:unknown-field`); + } + for (const key of EVIDENCE_KEYS) { + if (!(key in evidence)) findings.push(`${key}:missing`); + } + + if (evidence.schema !== NLOS_EVIDENCE_SCHEMA) findings.push('schema:mismatch'); + if (evidence.source !== 'LIVE_HARDWARE') findings.push('source:must-be-LIVE_HARDWARE'); + if (evidence.claim_tag !== 'MEASURED') findings.push('claim_tag:must-be-MEASURED'); + if (evidence.evidence_level !== 'L2') findings.push('evidence_level:must-be-L2'); + if (evidence.ground_truth !== 'EXTERNAL') findings.push('ground_truth:must-be-EXTERNAL'); + if (evidence.protocol_frozen_before_capture !== true) findings.push('protocol:not-preregistered'); + for (const field of [ + 'witness_reviewed', 'privacy_review_passed', 'security_review_passed', + 'los_exclusion_verified', 'independent_csi_verified', + ]) if (evidence[field] !== true) findings.push(`${field}:must-be-true`); + if (!SAFE_LABEL.test(String(evidence.sensor_model || ''))) findings.push('sensor_model:invalid'); + if (!/^VL53L8[A-Za-z0-9._:+-]*$/.test(String(evidence.sensor_model || ''))) { + findings.push('sensor_model:v1-requires-enrolled-external-ST-VL53L8-series'); + } + if (evidence.transient_kind !== 'COMPACT_NORMALIZED_HISTOGRAM' + && evidence.transient_kind !== 'RAW_PHOTON_HISTOGRAM') { + findings.push('transient_kind:unsupported'); + } + const sensorRateFinding = number( + evidence.sensor_configured_max_hz, 'sensor_configured_max_hz', { positive: true, max: 60 }, + ); + if (sensorRateFinding) findings.push(sensorRateFinding); + if (!GIT_SHA1.test(String(evidence.upstream_commit || '')) + || ZERO_GIT_SHA1.test(String(evidence.upstream_commit || ''))) { + findings.push('upstream_commit:expected-nonzero-full-sha1'); + } + for (const field of [ + 'protocol_sha256', 'capture_manifest_sha256', 'sensor_identity_sha256', + 'calibration_sha256', 'firmware_sha256', 'analysis_sha256', + 'endpoint_pairing_sha256', + 'sensor_configuration_sha256', 'witness_report_sha256', 'privacy_review_sha256', + 'security_review_sha256', 'guardrail_report_sha256', + 'csi_capture_manifest_sha256', 'csi_sensor_identity_sha256', 'csi_calibration_sha256', + ]) if (!isNonzeroSha256(evidence[field])) findings.push(`${field}:invalid-or-zero`); + if (evidence.synthetic_frames !== 0) findings.push('synthetic_frames:must-be-zero'); + if (evidence.replay_frames !== 0) findings.push('replay_frames:must-be-zero'); + if (!Number.isSafeInteger(evidence.paired_sequences) + || evidence.paired_sequences < 100 || evidence.paired_sequences > 1_000_000) { + findings.push('paired_sequences:out-of-range'); + } + if (!Number.isSafeInteger(evidence.csi_source_count) + || evidence.csi_source_count < 1 || evidence.csi_source_count > 256) { + findings.push('csi_source_count:out-of-range'); + } + if (!Number.isSafeInteger(evidence.offered_optical_frame_count) + || evidence.offered_optical_frame_count < 1 || evidence.offered_optical_frame_count > 1_000_000_000) { + findings.push('offered_optical_frame_count:out-of-range'); + } + findings.push(...validateArm(evidence.lidar_only, 'lidar_only', evidence.paired_sequences)); + findings.push(...validateArm(evidence.csi_only, 'csi_only', evidence.paired_sequences, { position: false })); + findings.push(...validateArm(evidence.fused, 'fused', evidence.paired_sequences)); + findings.push(...validateConfidence(evidence.confidence)); + findings.push(...validateGuardrails(evidence.guardrails)); + for (const name of ['lidar_only', 'fused']) { + const arm = evidence[name]; + if (arm && Number.isFinite(arm.evaluable_duration_s) && arm.evaluable_duration_s > 0 + && Number.isSafeInteger(arm.accepted_sensor_frame_count) + && Number.isFinite(evidence.sensor_configured_max_hz) + && arm.accepted_sensor_frame_count / arm.evaluable_duration_s > evidence.sensor_configured_max_hz + 1e-6) { + findings.push(`${name}.accepted_sensor_frame_rate:above-configured-maximum`); + } + if (arm && Number.isSafeInteger(arm.accepted_sensor_frame_count) + && Number.isSafeInteger(evidence.offered_optical_frame_count) + && arm.accepted_sensor_frame_count > evidence.offered_optical_frame_count) { + findings.push(`${name}.accepted_sensor_frame_count:above-offered-count`); + } + } + if (evidence.lidar_only && Number.isSafeInteger(evidence.offered_optical_frame_count) + && Number.isFinite(evidence.lidar_only.evaluable_duration_s) + && evidence.lidar_only.evaluable_duration_s > 0 + && Number.isFinite(evidence.sensor_configured_max_hz) + && evidence.offered_optical_frame_count / evidence.lidar_only.evaluable_duration_s + > evidence.sensor_configured_max_hz + 1e-6) { + findings.push('offered_optical_frame_rate:above-configured-maximum'); + } + if (evidence.fused && evidence.guardrails + && Number.isSafeInteger(evidence.offered_optical_frame_count) + && evidence.offered_optical_frame_count > 0 + && Number.isSafeInteger(evidence.fused.accepted_sensor_frame_count) + && Number.isFinite(evidence.guardrails.fused_frame_loss_rate)) { + const measuredLoss = (evidence.offered_optical_frame_count + - evidence.fused.accepted_sensor_frame_count) / evidence.offered_optical_frame_count; + if (!nearlyEqual(evidence.guardrails.fused_frame_loss_rate, measuredLoss)) { + findings.push('guardrails.fused_frame_loss_rate:arithmetic-mismatch'); + } + } + if (evidence.lidar_only && evidence.fused + && !nearlyEqual(evidence.lidar_only.evaluable_duration_s, evidence.fused.evaluable_duration_s)) { + findings.push('paired_optical_evaluable_duration:mismatch'); + } + if (evidence.lidar_only && evidence.fused + && !nearlyEqual(evidence.lidar_only.evaluable_track_duration_s, evidence.fused.evaluable_track_duration_s)) { + findings.push('lost_track_endpoint_pairing:evaluable-duration-mismatch'); + } + if (evidence.lidar_only && evidence.fused + && evidence.lidar_only.position_error_sample_count !== evidence.fused.position_error_sample_count) { + findings.push('position_endpoint_pairing:sample-count-mismatch'); + } + if (evidence.lidar_only && evidence.fused && evidence.guardrails) { + const updateRatio = evidence.lidar_only.update_hz > 0 + ? evidence.fused.update_hz / evidence.lidar_only.update_hz : 0; + const positionRegression = evidence.lidar_only.position_error_m > 0 + ? Math.max(0, (evidence.fused.position_error_m - evidence.lidar_only.position_error_m) + / evidence.lidar_only.position_error_m) + : (evidence.fused.position_error_m > 0 ? Infinity : 0); + const lostRegression = evidence.lidar_only.lost_track_rate > 0 + ? Math.max(0, (evidence.fused.lost_track_rate - evidence.lidar_only.lost_track_rate) + / evidence.lidar_only.lost_track_rate) + : (evidence.fused.lost_track_rate > 0 ? Infinity : 0); + if (Number.isFinite(evidence.guardrails.fused_update_rate_ratio_min) + && updateRatio < evidence.guardrails.fused_update_rate_ratio_min) { + findings.push('guardrails.fused_update_rate_ratio:below-frozen-limit'); + } + if (Number.isFinite(evidence.guardrails.nonwinning_position_error_regression_max) + && positionRegression > evidence.guardrails.nonwinning_position_error_regression_max) { + findings.push('guardrails.position_error_regression:above-frozen-limit'); + } + if (Number.isFinite(evidence.guardrails.nonwinning_lost_track_regression_max) + && lostRegression > evidence.guardrails.nonwinning_lost_track_regression_max) { + findings.push('guardrails.lost_track_regression:above-frozen-limit'); + } + } + if (findings.length) return { status: 'FAIL', pass: false, findings }; + + const baseError = evidence.lidar_only.position_error_m; + const fusedError = evidence.fused.position_error_m; + const baseLost = evidence.lidar_only.lost_track_rate; + const fusedLost = evidence.fused.lost_track_rate; + const positionErrorReduction = baseError > 0 ? (baseError - fusedError) / baseError : 0; + const lostTrackReduction = baseLost > 0 ? (baseLost - fusedLost) / baseLost : 0; + const reproductionPass = evidence.lidar_only.update_hz >= 27; + const positionEndpointPass = positionErrorReduction >= 0.25 + && evidence.confidence.position_error_reduction_lower > 0 + && positionErrorReduction >= evidence.confidence.position_error_reduction_lower + && positionErrorReduction <= evidence.confidence.position_error_reduction_upper; + const lostTrackEndpointPass = lostTrackReduction >= 0.25 + && evidence.confidence.lost_track_reduction_lower > 0 + && lostTrackReduction >= evidence.confidence.lost_track_reduction_lower + && lostTrackReduction <= evidence.confidence.lost_track_reduction_upper; + const fusionPass = positionEndpointPass || lostTrackEndpointPass; + const pass = reproductionPass && fusionPass; + return { + status: pass ? 'PASS' : 'FAIL', + pass, + reproduction: { + pass: reproductionPass, + threshold_hz: 27, + measured_hz: evidence.lidar_only.update_hz, + label: 'MEASURED', + }, + fusion: { + pass: fusionPass, + measured_hz: evidence.fused.update_hz, + threshold_fraction: 0.25, + position_error_reduction: positionErrorReduction, + lost_track_reduction: lostTrackReduction, + position_endpoint_pass: positionEndpointPass, + lost_track_endpoint_pass: lostTrackEndpointPass, + adjusted_confidence_level: evidence.confidence.familywise_confidence_level, + rule: 'position_error_reduction OR lost_track_reduction', + label: 'MEASURED', + }, + paired_sequences: evidence.paired_sequences, + capture_manifest_sha256: evidence.capture_manifest_sha256, + claim_tag: evidence.claim_tag, + evidence_level: evidence.evidence_level, + }; +} + +function readEvidence(repo, evidenceFile) { + if (!evidenceFile) return { evidence: null, path: null }; + const candidate = resolve(repo, String(evidenceFile)); + if (!existsSync(candidate)) throw new Error('evidence_file_missing'); + const canonicalRepo = realpathSync(repo); + const canonicalFile = realpathSync(candidate); + const rel = relative(canonicalRepo, canonicalFile); + if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error('evidence_file_outside_repo'); + const stat = statSync(canonicalFile); + if (!stat.isFile()) throw new Error('evidence_file_not_regular'); + if (stat.size > MAX_EVIDENCE_BYTES) throw new Error('evidence_file_too_large'); + let evidence; + try { evidence = JSON.parse(readFileSync(canonicalFile, 'utf8')); } + catch { throw new Error('evidence_file_malformed_json'); } + return { evidence, path: rel.replaceAll('\\', '/') }; +} + +export function makeNlosPlan({ repoRoot = null } = {}) { + return { + ok: true, + advisory: true, + title: 'RuView consumer NLOS implementation plan', + invariant: 'Preserve per-zone photon timing histograms; processed depth maps cannot substitute for transient samples.', + phases: [ + { + id: 1, name: 'Upstream reproduction', + input: 'Pinned upstream-compatible ST kit with verified compact normalized histogram access and recorded silicon identity', + exit: 'LIVE_HARDWARE hidden-target tracking at >=27 Hz with frozen capture provenance', + }, + { + id: 2, name: 'RuView temporal state', + input: 'Calibrated transient frames, pose, wall geometry, and canonical target response', + exit: 'ruview.nlos.track.v1 hypotheses with covariance, freshness, calibration, and evidence labels', + }, + { + id: 3, name: 'CSI fusion', + input: 'Synchronized optical likelihood and CSI likelihood in one world frame', + exit: 'Paired preregistered comparison; >=25% lower position error OR lost-track rate', + }, + { + id: 4, name: 'Native and web iOS adapters', + input: 'Authenticated versioned RuView tracks; ARKit scene depth only as pose/context unless raw transients become public', + exit: 'Native build and authenticated web transport/replay pass without an iPhone-NLOS claim', + }, + ], + surfaces: SURFACES.map(({ id, base, marker }) => ({ id, base, marker })), + verifier: { + static: 'ruview nlos verify --repo ', + builds: 'ruview nlos verify --repo --run-builds', + research: `ruview nlos verify --repo --evidence-file <${NLOS_EVIDENCE_SCHEMA}.json> --require-research-pass`, + }, + repo_detected: Boolean(repoRoot), + repo_root: repoRoot, + }; +} + +export async function verifyNlos(args = {}, { + repoRoot = null, + source = 'internal', + runCommand = null, + whichCommand = null, +} = {}) { + if (source === 'mcp' && (args.repo !== undefined || args.run_builds === true)) { + return { ok: false, reason: 'cli_only_repo_or_builds', advisory: true }; + } + const repo = args.repo ? resolve(args.repo) : repoRoot; + if (!repo) return { ok: false, reason: 'not_in_ruview_repo' }; + const isRepo = existsSync(join(repo, 'v2/Cargo.toml')) + || existsSync(join(repo, 'archive/v1/data/proof/verify.py')); + if (!isRepo) return { ok: false, reason: 'not_in_ruview_repo' }; + + const surfaces = SURFACES.map((surface) => inspectSurface(repo, surface)); + const malformed = surfaces.filter((surface) => surface.status === 'MALFORMED'); + let loaded; + try { loaded = readEvidence(repo, args.evidence_file); } + catch (error) { + return { ok: false, reason: String(error.message || error), surfaces, advisory: true }; + } + const research = evaluateResearchEvidence(loaded.evidence); + if (args.run_builds === true && (!runCommand || !whichCommand)) { + return { ok: false, reason: 'build_executor_unavailable', surfaces, advisory: true }; + } + const builds = args.run_builds === true + ? await runBuilds(repo, surfaces, { runCommand, whichCommand }) + : []; + const buildFailures = builds.filter((result) => result.status === 'FAIL'); + const requireResearch = args.require_research_pass === true; + const evidenceSupplied = loaded.path !== null; + const researchMustPass = requireResearch || evidenceSupplied; + const ok = malformed.length === 0 && buildFailures.length === 0 + && (!researchMustPass || research.pass); + const completedBuilds = builds.filter((result) => result.status !== 'SKIPPED'); + let softwareStatus = 'STATIC_DISCOVERY_ONLY'; + if (malformed.length || buildFailures.length) softwareStatus = 'FAIL'; + else if (args.run_builds && builds.length === 0) softwareStatus = 'NO_PRESENT_BUILD_SURFACES'; + else if (args.run_builds && completedBuilds.length === 0) softwareStatus = 'NO_BUILD_TOOLCHAINS_AVAILABLE'; + else if (args.run_builds && builds.some((result) => result.status === 'SKIPPED')) { + softwareStatus = 'AVAILABLE_CHECKS_PASS_WITH_SKIPS'; + } else if (args.run_builds) softwareStatus = 'AVAILABLE_CHECKS_PASS'; + + return { + ok, + advisory: true, + meta_harness_required_by_runtime: false, + track_schema: NLOS_TRACK_SCHEMA, + surfaces, + software_gate: { + status: softwareStatus, + malformed: malformed.map((surface) => surface.id), + build_failures: buildFailures.map((result) => result.id), + builds, + }, + research_gate: { ...research, evidence_file: loaded.path }, + note: 'Static discovery and available-build results are advisory subsets of Gate A. They are never hardware or release validation.', + build_authority: args.run_builds ? 'OPT_IN_LOCAL_CLI_TRUSTED_CHECKOUT' : 'NONE', + }; +} diff --git a/harness/ruview/src/policy.js b/harness/ruview/src/policy.js index 62eae4c7..be6d9a45 100644 --- a/harness/ruview/src/policy.js +++ b/harness/ruview/src/policy.js @@ -9,6 +9,8 @@ export const TOOL_POLICY = Object.freeze({ ruview_calibrate: { class: 'workspace-write', writesWorkspace: true, confirmField: 'confirm' }, ruview_node_flash: { class: 'hardware-write', writesWorkspace: true, hardware: true, confirmField: 'confirm' }, ruview_guidance: { class: 'read', readOnly: true }, + ruview_nlos_plan: { class: 'read', readOnly: true }, + ruview_nlos_verify: { class: 'execute', readOnly: true }, ruview_spaces_list: { class: 'external-read', readOnly: true, requiredGrant: 'credential-use', openWorld: true, usesCredentials: true, mayRefreshCredentials: true }, ruview_memory_search: { class: 'read', readOnly: true }, }); diff --git a/harness/ruview/src/tools.js b/harness/ruview/src/tools.js index 080e6390..a61c5416 100644 --- a/harness/ruview/src/tools.js +++ b/harness/ruview/src/tools.js @@ -21,6 +21,7 @@ import { authorizeTool, mcpAnnotations, validateArguments } from './policy.js'; import { searchBrain } from './brain.js'; import { getGuidance, GUIDANCE_TOPICS } from './guidance.js'; import { listCognitumSpaces } from './spaces.js'; +import { makeNlosPlan, verifyNlos } from './nlos.js'; /** Walk up from `start` to find the RuView monorepo root (or null). */ export function findRepoRoot(start = process.cwd()) { @@ -77,7 +78,11 @@ export function run(cmd, args, opts = {}) { let stderr = ''; let child; try { - child = spawn(cmd, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] }); + child = spawn(cmd, args, { + cwd: opts.cwd, + env: opts.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); } catch (e) { resolvePromise({ status: null, ok: false, stdout: '', stderr: '', error: e.message }); return; @@ -291,6 +296,34 @@ export const TOOLS = { }, }, + ruview_nlos_plan: { + title: 'Plan consumer NLOS work', + description: 'Return the staged, source-bounded RuView consumer-NLOS plan. Advisory only: it never captures hardware, mutates the repository, or claims a research result.', + inputSchema: { type: 'object', properties: {} }, + handler() { + return makeNlosPlan({ repoRoot: findRepoRoot() }); + }, + }, + + ruview_nlos_verify: { + title: 'Verify consumer NLOS surfaces and evidence', + description: 'Inspect optional Rust/native/web NLOS surfaces and evaluate a preregistered live-hardware evidence record. The local CLI can also run available build gates; MCP rejects repository selection and build execution. Missing optional surfaces are reported, not fabricated; malformed present surfaces fail.', + inputSchema: { + type: 'object', + properties: { + repo: { type: 'string', minLength: 1, maxLength: 4096, description: 'CLI only: RuView repository root. Default: auto-detect from cwd.' }, + evidence_file: { type: 'string', minLength: 1, maxLength: 4096, description: 'Repository-contained ruview.nlos.acceptance.v1 JSON evidence record.' }, + run_builds: { type: 'boolean', description: 'CLI only: run available Rust, Swift/Xcode, and web gates. Unavailable toolchains are explicit skips.' }, + require_research_pass: { type: 'boolean', description: 'Fail unless live-hardware reproduction and fusion acceptance both pass.' }, + }, + }, + async handler(args = {}, context = {}) { + return verifyNlos(args, { + repoRoot: findRepoRoot(), source: context.source, runCommand: run, whichCommand: which, + }); + }, + }, + ruview_spaces_list: { title: 'List Cognitum Spatial Resources', description: 'Page sites, buildings, floors, spaces, zones, anonymous entities, semantic events, or alerts in the authenticated tenant/workspace through the hardened wifi-densepose OAuth client. Never accepts tokens, API keys, writes, approvals, or action authority.', diff --git a/harness/ruview/test/mcp.test.mjs b/harness/ruview/test/mcp.test.mjs index a6668e23..78faa2eb 100644 --- a/harness/ruview/test/mcp.test.mjs +++ b/harness/ruview/test/mcp.test.mjs @@ -51,11 +51,17 @@ test('MCP handshake: initialize reports the package.json version; list endpoints s.send({ jsonrpc: '2.0', id: 2, method: 'tools/list' }); const tools = (await s.next(2)).result.tools; - assert.equal(tools.length, 9); + assert.equal(tools.length, 11); for (const t of tools) assert.match(t.name, /^[a-zA-Z0-9_-]{1,64}$/, `advertised name not host-safe: ${t.name}`); const guidance = tools.find((tool) => tool.name === 'ruview_guidance'); assert.ok(guidance); assert.equal(guidance.annotations.readOnlyHint, true); + const nlosPlan = tools.find((tool) => tool.name === 'ruview_nlos_plan'); + const nlosVerify = tools.find((tool) => tool.name === 'ruview_nlos_verify'); + assert.ok(nlosPlan); + assert.ok(nlosVerify); + assert.equal(nlosPlan.annotations.readOnlyHint, true); + assert.equal(nlosVerify.annotations.readOnlyHint, true); const spaces = tools.find((tool) => tool.name === 'ruview_spaces_list'); assert.ok(spaces); assert.equal(spaces.annotations.readOnlyHint, false, 'OAuth refresh can update the local credential file'); diff --git a/harness/ruview/test/nlos.test.mjs b/harness/ruview/test/nlos.test.mjs new file mode 100644 index 00000000..d5292e05 --- /dev/null +++ b/harness/ruview/test/nlos.test.mjs @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: MIT +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { evaluateResearchEvidence, NLOS_EVIDENCE_SCHEMA, NLOS_TRACK_SCHEMA } from '../src/nlos.js'; +import { runTool } from '../src/tools.js'; +import { run as cliRun } from '../bin/cli.js'; + +const DIGEST = 'a'.repeat(64); +const UPSTREAM_COMMIT = '0123456789abcdef0123456789abcdef01234567'; + +function arm(updateHz, positionErrorM, lostTrackRate) { + const evaluableDurationS = 100; + const acceptedUpdateCount = Math.round(updateHz * evaluableDurationS); + return { + sequence_count: 100, + accepted_sensor_frame_count: 3_000, + accepted_update_count: acceptedUpdateCount, + evaluable_duration_s: evaluableDurationS, + update_hz: acceptedUpdateCount / evaluableDurationS, + position_error_sum_m: positionErrorM * 100, + position_error_sample_count: 100, + position_error_m: positionErrorM, + lost_track_duration_s: lostTrackRate * evaluableDurationS, + evaluable_track_duration_s: evaluableDurationS, + lost_track_rate: lostTrackRate, + }; +} + +function csiArm(updateHz, lostTrackRate) { + const value = arm(updateHz, 0, lostTrackRate); + delete value.position_error_sum_m; + delete value.position_error_sample_count; + delete value.position_error_m; + return value; +} + +function passingEvidence(overrides = {}) { + return { + schema: NLOS_EVIDENCE_SCHEMA, + source: 'LIVE_HARDWARE', + claim_tag: 'MEASURED', + evidence_level: 'L2', + ground_truth: 'EXTERNAL', + protocol_frozen_before_capture: true, + witness_reviewed: true, + privacy_review_passed: true, + security_review_passed: true, + los_exclusion_verified: true, + independent_csi_verified: true, + sensor_model: 'VL53L8CH', + transient_kind: 'COMPACT_NORMALIZED_HISTOGRAM', + sensor_configured_max_hz: 30, + upstream_commit: UPSTREAM_COMMIT, + protocol_sha256: DIGEST, + capture_manifest_sha256: DIGEST, + sensor_identity_sha256: DIGEST, + calibration_sha256: DIGEST, + firmware_sha256: DIGEST, + analysis_sha256: DIGEST, + endpoint_pairing_sha256: DIGEST, + sensor_configuration_sha256: DIGEST, + witness_report_sha256: DIGEST, + privacy_review_sha256: DIGEST, + security_review_sha256: DIGEST, + guardrail_report_sha256: DIGEST, + csi_capture_manifest_sha256: DIGEST, + csi_sensor_identity_sha256: DIGEST, + csi_calibration_sha256: DIGEST, + synthetic_frames: 0, + replay_frames: 0, + paired_sequences: 100, + csi_source_count: 1, + offered_optical_frame_count: 3_000, + lidar_only: arm(29.4, 0.12, 0.20), + csi_only: csiArm(15, 0.40), + fused: arm(28.9, 0.089, 0.19), + confidence: { + bootstrap_resamples: 10_000, + bootstrap_seed_list_sha256: DIGEST, + familywise_confidence_level: 0.975, + position_error_reduction_lower: 0.01, + position_error_reduction_upper: 0.40, + lost_track_reduction_lower: -0.10, + lost_track_reduction_upper: 0.20, + }, + guardrails: { + empty_false_track_rate: 0, + empty_false_track_rate_max: 0.01, + fused_p95_latency_ms: 80, + fused_p95_latency_max_ms: 100, + fused_frame_loss_rate: 0, + fused_frame_loss_rate_max: 0.05, + exclusion_fraction: 0.02, + exclusion_fraction_max: 0.05, + fused_update_rate_ratio_min: 0.25, + nonwinning_position_error_regression_max: 0.10, + nonwinning_lost_track_regression_max: 0.10, + }, + ...overrides, + }; +} + +test('nlos plan is advisory, staged, and names the canonical track schema', async () => { + const result = await runTool('ruview_nlos_plan', {}); + assert.equal(result.ok, true); + assert.equal(result.advisory, true); + assert.equal(result.phases.length, 4); + assert.match(JSON.stringify(result), new RegExp(NLOS_TRACK_SCHEMA.replaceAll('.', '\\.'))); +}); + +test('research gate accepts live hardware at roughly 30 Hz and either 25% fusion gain', () => { + const result = evaluateResearchEvidence(passingEvidence()); + assert.equal(result.pass, true, JSON.stringify(result)); + assert.equal(result.reproduction.pass, true); + assert.ok(result.fusion.position_error_reduction >= 0.25); + + const lostTrackWin = passingEvidence({ + lidar_only: arm(27, 0.10, 0.20), + fused: arm(19, 0.10, 0.14), + confidence: { + bootstrap_resamples: 10_000, + bootstrap_seed_list_sha256: DIGEST, + familywise_confidence_level: 0.975, + position_error_reduction_lower: -0.10, + position_error_reduction_upper: 0.10, + lost_track_reduction_lower: 0.01, + lost_track_reduction_upper: 0.50, + }, + }); + assert.equal(evaluateResearchEvidence(lostTrackWin).pass, true); +}); + +test('research gate never accepts synthetic or replay evidence', () => { + for (const evidence of [ + passingEvidence({ source: 'SYNTHETIC' }), + passingEvidence({ synthetic_frames: 1 }), + passingEvidence({ replay_frames: 1 }), + passingEvidence({ claim_tag: 'SYNTHETIC', evidence_level: 'L0' }), + ]) { + const result = evaluateResearchEvidence(evidence); + assert.equal(result.pass, false, JSON.stringify(result)); + assert.equal(result.status, 'FAIL'); + } +}); + +test('research gate fails sub-rate reproduction and sub-threshold fusion', () => { + const slow = passingEvidence({ + lidar_only: arm(26.9, 0.12, 0.20), + }); + assert.equal(evaluateResearchEvidence(slow).reproduction.pass, false); + + const noGain = passingEvidence({ + fused: arm(29, 0.091, 0.16), + }); + assert.equal(evaluateResearchEvidence(noGain).pass, false); + + const slowFusion = passingEvidence({ fused: arm(12, 0.08, 0.19) }); + assert.equal(evaluateResearchEvidence(slowFusion).pass, true, 'fused rate is reported but is not the user hard gate'); +}); + +test('research evidence rejects unknown and incomplete fields', () => { + const extraTopLevel = evaluateResearchEvidence(passingEvidence({ bearer_token: 'must-not-be-here' })); + assert.equal(extraTopLevel.pass, false); + assert.ok(extraTopLevel.findings.includes('bearer_token:unknown-field')); + + const incompleteArm = passingEvidence(); + delete incompleteArm.fused.sequence_count; + const missing = evaluateResearchEvidence(incompleteArm); + assert.equal(missing.pass, false); + assert.ok(missing.findings.includes('fused.sequence_count:missing')); + + const impossible = passingEvidence({ sensor_identity_sha256: '0'.repeat(64) }); + impossible.lidar_only.update_hz = 1e300; + const bounded = evaluateResearchEvidence(impossible); + assert.equal(bounded.pass, false); + assert.ok(bounded.findings.includes('sensor_identity_sha256:invalid-or-zero')); + assert.ok(bounded.findings.includes('lidar_only.update_hz:above-maximum')); + + const appleClaim = evaluateResearchEvidence(passingEvidence({ sensor_model: 'iPhone15Pro' })); + assert.equal(appleClaim.pass, false); + assert.ok(appleClaim.findings.includes('sensor_model:v1-requires-enrolled-external-ST-VL53L8-series')); + + const zeroCommit = evaluateResearchEvidence(passingEvidence({ upstream_commit: '0'.repeat(40) })); + assert.equal(zeroCommit.pass, false); + assert.ok(zeroCommit.findings.includes('upstream_commit:expected-nonzero-full-sha1')); +}); + +test('research evidence recomputes loss and requires powered paired endpoint coverage', () => { + const falseLoss = passingEvidence(); + falseLoss.fused.accepted_sensor_frame_count = 2_700; + falseLoss.guardrails.fused_frame_loss_rate = 0; + const lossResult = evaluateResearchEvidence(falseLoss); + assert.equal(lossResult.pass, false); + assert.ok(lossResult.findings.includes('guardrails.fused_frame_loss_rate:arithmetic-mismatch')); + + const impossibleOfferRate = passingEvidence({ offered_optical_frame_count: 3_001 }); + impossibleOfferRate.lidar_only.accepted_sensor_frame_count = 3_001; + impossibleOfferRate.fused.accepted_sensor_frame_count = 3_001; + const offerResult = evaluateResearchEvidence(impossibleOfferRate); + assert.equal(offerResult.pass, false); + assert.ok(offerResult.findings.includes('offered_optical_frame_rate:above-configured-maximum')); + + const oneOfOneHundred = passingEvidence(); + oneOfOneHundred.lidar_only.position_error_sample_count = 1; + oneOfOneHundred.lidar_only.position_error_sum_m = 0.12; + oneOfOneHundred.fused.position_error_sample_count = 1; + oneOfOneHundred.fused.position_error_sum_m = 0.089; + const coverageResult = evaluateResearchEvidence(oneOfOneHundred); + assert.equal(coverageResult.pass, false); + assert.ok(coverageResult.findings.includes( + 'lidar_only.position_error_sample_count:must-equal-paired-sequences', + )); + + const mismatchedTrackDenominator = passingEvidence(); + mismatchedTrackDenominator.fused.evaluable_track_duration_s = 10; + mismatchedTrackDenominator.fused.lost_track_duration_s = 1.5; + mismatchedTrackDenominator.fused.lost_track_rate = 0.15; + const pairingResult = evaluateResearchEvidence(mismatchedTrackDenominator); + assert.equal(pairingResult.pass, false); + assert.ok(pairingResult.findings.includes('lost_track_endpoint_pairing:evaluable-duration-mismatch')); +}); + +test('nlos verify degrades cleanly when all optional feature surfaces are absent', async () => { + const repo = mkdtempSync(join(tmpdir(), 'ruview-nlos-absent-')); + try { + mkdirSync(join(repo, 'v2'), { recursive: true }); + writeFileSync(join(repo, 'v2', 'Cargo.toml'), '[workspace]\n'); + const result = await runTool('ruview_nlos_verify', { repo }); + assert.equal(result.ok, true, JSON.stringify(result)); + assert.ok(result.surfaces.every((surface) => surface.status === 'ABSENT')); + assert.equal(result.research_gate.status, 'NOT_EVALUATED'); + assert.equal(result.meta_harness_required_by_runtime, false); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + +test('nlos verify fails a malformed present surface', async () => { + const repo = mkdtempSync(join(tmpdir(), 'ruview-nlos-malformed-')); + try { + mkdirSync(join(repo, 'v2', 'crates', 'ruview-nlos'), { recursive: true }); + writeFileSync(join(repo, 'v2', 'Cargo.toml'), '[workspace]\n'); + writeFileSync(join(repo, 'v2', 'crates', 'ruview-nlos', 'Cargo.toml'), '[package]\nname="wrong"\n'); + const result = await runTool('ruview_nlos_verify', { repo }); + assert.equal(result.ok, false); + assert.equal(result.surfaces.find((surface) => surface.id === 'rust-core').status, 'MALFORMED'); + } finally { rmSync(repo, { recursive: true, force: true }); } +}); + +test('nlos evidence is confined, bounded, and any supplied failing record fails closed', async () => { + const repo = mkdtempSync(join(tmpdir(), 'ruview-nlos-evidence-')); + const outside = join(tmpdir(), `ruview-nlos-outside-${process.pid}.json`); + try { + mkdirSync(join(repo, 'v2'), { recursive: true }); + writeFileSync(join(repo, 'v2', 'Cargo.toml'), '[workspace]\n'); + writeFileSync(join(repo, 'evidence.json'), JSON.stringify(passingEvidence())); + writeFileSync(join(repo, 'failing.json'), JSON.stringify(passingEvidence({ source: 'SYNTHETIC' }))); + writeFileSync(join(repo, 'malformed.json'), '{"schema":'); + writeFileSync(join(repo, 'oversize.json'), ' '.repeat(1024 * 1024 + 1)); + writeFileSync(outside, JSON.stringify(passingEvidence())); + + const pass = await runTool('ruview_nlos_verify', { + repo, evidence_file: 'evidence.json', require_research_pass: true, + }); + assert.equal(pass.ok, true, JSON.stringify(pass)); + assert.equal(pass.research_gate.status, 'PASS'); + + const failing = await runTool('ruview_nlos_verify', { repo, evidence_file: 'failing.json' }); + assert.equal(failing.ok, false); + assert.equal(failing.research_gate.status, 'FAIL'); + + const malformed = await runTool('ruview_nlos_verify', { repo, evidence_file: 'malformed.json' }); + assert.equal(malformed.ok, false); + assert.equal(malformed.reason, 'evidence_file_malformed_json'); + + const oversized = await runTool('ruview_nlos_verify', { repo, evidence_file: 'oversize.json' }); + assert.equal(oversized.ok, false); + assert.equal(oversized.reason, 'evidence_file_too_large'); + + const escaped = await runTool('ruview_nlos_verify', { repo, evidence_file: outside }); + assert.equal(escaped.ok, false); + assert.equal(escaped.reason, 'evidence_file_outside_repo'); + } finally { + rmSync(repo, { recursive: true, force: true }); + rmSync(outside, { force: true }); + } +}); + +test('MCP NLOS verification cannot select a repository or execute build tools', async () => { + const selected = await runTool('ruview_nlos_verify', { repo: '/tmp' }, { source: 'mcp' }); + assert.equal(selected.ok, false); + assert.equal(selected.reason, 'cli_only_repo_or_builds'); + const builds = await runTool('ruview_nlos_verify', { run_builds: true }, { source: 'mcp' }); + assert.equal(builds.ok, false); + assert.equal(builds.reason, 'cli_only_repo_or_builds'); +}); + +test('nlos CLI nested commands reject unknown actions', async () => { + assert.equal(await cliRun(['nlos', 'plan']), 0); + assert.equal(await cliRun(['nlos', 'definitely-unknown']), 2); +}); diff --git a/harness/ruview/test/policy.test.mjs b/harness/ruview/test/policy.test.mjs index 47fa09bd..663c77fe 100644 --- a/harness/ruview/test/policy.test.mjs +++ b/harness/ruview/test/policy.test.mjs @@ -19,6 +19,8 @@ test('MCP workspace writes require confirmation and an explicit grant', () => { test('read-only tools remain available with no mutation grants', () => { assert.equal(authorizeTool('ruview_claim_check', { text: 'safe' }, { source: 'mcp', grants: [] }).ok, true); assert.equal(authorizeTool('ruview_guidance', {}, { source: 'mcp', grants: [] }).ok, true); + assert.equal(authorizeTool('ruview_nlos_plan', {}, { source: 'mcp', grants: [] }).ok, true); + assert.equal(authorizeTool('ruview_nlos_verify', {}, { source: 'mcp', grants: [] }).ok, true); assert.deepEqual(validateArguments({ type: 'object', properties: {} }, {}), []); }); diff --git a/harness/ruview/test/tools.test.mjs b/harness/ruview/test/tools.test.mjs index 3e9be52a..849da00c 100644 --- a/harness/ruview/test/tools.test.mjs +++ b/harness/ruview/test/tools.test.mjs @@ -93,7 +93,7 @@ test('summarize gives PASS/finding text', () => { test('registry exposes the documented tools with schemas (underscore-canonical)', () => { const names = Object.keys(TOOLS); - for (const n of ['ruview_onboard', 'ruview_claim_check', 'ruview_verify', 'ruview_node_monitor', 'ruview_calibrate', 'ruview_node_flash', 'ruview_guidance', 'ruview_spaces_list', 'ruview_memory_search']) { + for (const n of ['ruview_onboard', 'ruview_claim_check', 'ruview_verify', 'ruview_node_monitor', 'ruview_calibrate', 'ruview_node_flash', 'ruview_guidance', 'ruview_nlos_plan', 'ruview_nlos_verify', 'ruview_spaces_list', 'ruview_memory_search']) { assert.ok(names.includes(n), `missing ${n}`); assert.equal(TOOLS[n].inputSchema.type, 'object'); assert.match(n, /^[a-zA-Z0-9_-]{1,64}$/, 'canonical names must satisfy host tool-name regexes'); diff --git a/ui/ios-nlos/.gitignore b/ui/ios-nlos/.gitignore new file mode 100644 index 00000000..4311a15f --- /dev/null +++ b/ui/ios-nlos/.gitignore @@ -0,0 +1,5 @@ +.build/ +.swiftpm/ +DerivedData/ +xcuserdata/ +*.xcuserstate diff --git a/ui/ios-nlos/App/AppModel.swift b/ui/ios-nlos/App/AppModel.swift new file mode 100644 index 00000000..acaca034 --- /dev/null +++ b/ui/ios-nlos/App/AppModel.swift @@ -0,0 +1,147 @@ +import Combine +import Foundation +import RuViewNLOSApple +import RuViewNLOSCore + +@MainActor +final class AppModel: ObservableObject { + enum ConnectionState: Equatable { + case disconnected + case connecting + case connected + case blocked + } + + @Published var endpointText = "" { + didSet { + if endpointText != oldValue { + storedTokenAvailable = false + } + } + } + @Published var pairingToken = "" + @Published private(set) var storedTokenAvailable = false + @Published private(set) var transportActive = false + @Published private(set) var connectionState: ConnectionState = .disconnected + @Published private(set) var statusMessage = "Disconnected; no track evidence is displayed." + @Published private(set) var frame: TrackDisplayFrame? + + let capabilities = AppleCapabilityProbe.probe() + + private let client = NLOSWebSocketClient() + private let tokenStore = KeychainPairingTokenStore() + + init() { + client.onEvent = { [weak self] event in + self?.handle(event) + } + } + + var tracks: [NLOSTrack] { frame?.tracks ?? [] } + var isConnected: Bool { transportActive } + + func connect() { + if transportActive { + client.disconnect() + } + let trimmedEndpoint = endpointText.trimmingCharacters(in: .whitespacesAndNewlines) + guard let endpoint = URL(string: trimmedEndpoint) else { + block("Enter a valid wss endpoint.") + return + } + + do { + let enteredToken = pairingToken + let token: String + if enteredToken.isEmpty { + guard let stored = try tokenStore.load(for: endpoint) else { + storedTokenAvailable = false + block("Enter a pairing token. None is stored for this server authority.") + return + } + storedTokenAvailable = true + token = stored + } else { + token = enteredToken + } + + try WSSConnectionValidator.validate(endpoint: endpoint, pairingToken: token) + if !enteredToken.isEmpty { + try tokenStore.save(enteredToken, for: endpoint) + storedTokenAvailable = true + } + try client.connect(endpoint: endpoint, pairingToken: token) + pairingToken = "" + } catch let error as LocalizedError { + block(error.errorDescription ?? "Connection setup failed closed.") + } catch { + block("Connection setup failed closed.") + } + } + + func disconnect() { + client.disconnect() + } + + func suspendForPrivacy() { + guard isConnected || frame != nil else { return } + client.disconnect() + } + + func forgetPairingToken() { + do { + if transportActive { + client.disconnect() + } + try tokenStore.deleteAll() + pairingToken = "" + storedTokenAvailable = false + statusMessage = "Pairing token removed from Keychain." + } catch { + block("Keychain token could not be removed.") + } + } + + private func handle(_ event: NLOSStreamEvent) { + switch event { + case .connecting: + transportActive = true + connectionState = .connecting + clearFrame() + statusMessage = "Opening authenticated secure stream…" + case .connected: + transportActive = true + connectionState = .connected + statusMessage = "Secure stream connected; waiting for validated evidence." + case let .frame(displayFrame): + transportActive = true + connectionState = .connected + frame = displayFrame + if displayFrame.tracks.isEmpty { + statusMessage = "Frame accepted, but no displayable tracks were present. Unknown tracks remain hidden." + } else { + statusMessage = "Validated frame \(displayFrame.sequence) with \(displayFrame.tracks.count) displayable track(s)." + } + case let .failClosed(reason): + connectionState = .blocked + clearFrame() + statusMessage = reason + case let .disconnected(reason): + transportActive = false + connectionState = .disconnected + clearFrame() + statusMessage = reason + } + } + + private func block(_ reason: String) { + transportActive = false + connectionState = .blocked + clearFrame() + statusMessage = reason + } + + private func clearFrame() { + frame = nil + } +} diff --git a/ui/ios-nlos/App/ContentView.swift b/ui/ios-nlos/App/ContentView.swift new file mode 100644 index 00000000..06c394f1 --- /dev/null +++ b/ui/ios-nlos/App/ContentView.swift @@ -0,0 +1,256 @@ +import Foundation +import RuViewNLOSApple +import RuViewNLOSCore +import SwiftUI + +struct ContentView: View { + @ObservedObject var model: AppModel + @Environment(\.scenePhase) private var scenePhase + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 18) { + connectionCard + statusCard + visualizationCard + capabilityCard + boundaryCard + } + .padding() + } + .navigationTitle("RuView NLOS") + .background(Color(uiColor: .systemGroupedBackground)) + .onChange(of: scenePhase) { phase in + if phase != .active { + model.suspendForPrivacy() + } + } + } + } + + private var connectionCard: some View { + card(title: "Authenticated stream") { + TextField("wss://host.example/api/v1/nlos/ws", text: $model.endpointText) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.URL) + .textContentType(.URL) + .padding(12) + .background(Color(uiColor: .tertiarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + SecureField( + model.storedTokenAvailable ? "Pairing token stored in Keychain" : "Pairing token", + text: $model.pairingToken + ) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .textContentType(.password) + .padding(12) + .background(Color(uiColor: .tertiarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + HStack { + Button(model.isConnected ? "Reconnect" : "Connect") { + model.connect() + } + .buttonStyle(.borderedProminent) + + if model.isConnected { + Button("Disconnect", role: .cancel) { + model.disconnect() + } + .buttonStyle(.bordered) + } + + Spacer() + + if model.storedTokenAvailable { + Button("Forget token", role: .destructive) { + model.forgetPairingToken() + } + .font(.caption) + } + } + + Label( + "Bearer token stays in this device's Keychain and is sent only over wss.", + systemImage: "lock.shield" + ) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + private var statusCard: some View { + card(title: "Evidence status") { + HStack(alignment: .top) { + Circle() + .fill(statusColor) + .frame(width: 10, height: 10) + .padding(.top, 4) + Text(model.statusMessage) + .font(.subheadline) + Spacer() + } + + if let frame = model.frame { + HStack(spacing: 8) { + badge(frame.source.rawValue.uppercased(), color: sourceColor(frame.source)) + badge(frame.evidenceLevel.rawValue.uppercased(), color: .indigo) + badge("SEQ \(frame.sequence)", color: .gray) + } + .accessibilityElement(children: .combine) + + VStack(alignment: .leading, spacing: 4) { + Text("Sensor: \(frame.provenance.sensorModel)") + Text("Transient: \(frame.provenance.transientKind.rawValue)") + Text("Histogram preserved: \(frame.provenance.histogramPreserved ? "yes" : "no")") + Text("Algorithm: \(frame.algorithmVersion)") + } + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + } + } + } + + private var visualizationCard: some View { + card(title: "Validated hidden target hypotheses") { + ZStack { + TrackCanvas(tracks: model.tracks) + .frame(height: 300) + .privacySensitive() + + if let watermark = model.frame?.watermark { + Text(watermark) + .font(.system(size: 38, weight: .black, design: .rounded)) + .foregroundStyle(.orange.opacity(0.42)) + .rotationEffect(.degrees(-18)) + .accessibilityLabel("Synthetic evidence watermark") + } + + if model.tracks.isEmpty { + Text("NO DISPLAYABLE TRACKS") + .font(.caption.bold().monospaced()) + .foregroundStyle(.secondary) + .padding(10) + .background(.ultraThinMaterial, in: Capsule()) + } + } + + ForEach(model.tracks) { track in + HStack { + VStack(alignment: .leading) { + Text(track.trackId) + .font(.subheadline.monospaced()) + .lineLimit(1) + Text(String( + format: "x %.2f y %.2f z %.2f m", + track.positionM.x, + track.positionM.y, + track.positionM.z + )) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + Spacer() + Text("\(Int(track.confidence * 100))%") + .font(.headline.monospacedDigit()) + badge(track.state.rawValue.uppercased(), color: track.state == .degraded ? .orange : .cyan) + } + .accessibilityElement(children: .combine) + } + } + .privacySensitive() + } + + private var capabilityCard: some View { + card(title: "Apple capability probe") { + capabilityRow("ARKit scene depth", model.capabilities.sceneDepth) + capabilityRow("ARKit smoothed depth", model.capabilities.smoothedSceneDepth) + capabilityRow("ARKit scene mesh", model.capabilities.sceneMesh) + capabilityRow("ARKit world pose", model.capabilities.worldPose) + capabilityRow("Raw photon histograms", model.capabilities.rawPhotonHistograms) + + Text(model.capabilities.rawPhotonHistogramReason) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + + private var boundaryCard: some View { + card(title: "Interpretation boundary") { + Label( + "This client visualizes validated NLOS output produced by an external transient histogram pipeline.", + systemImage: "waveform.path.ecg.rectangle" + ) + Label( + "ARKit depth, mesh, and pose are useful context, but this app never labels them as optical NLOS evidence.", + systemImage: "exclamationmark.shield" + ) + Text("Unknown, stale, malformed, replayed, oversized, or unauthenticated input is hidden by default.") + .font(.caption.bold()) + } + .font(.subheadline) + } + + private var statusColor: Color { + switch model.connectionState { + case .connected: return .green + case .connecting: return .yellow + case .blocked: return .red + case .disconnected: return .secondary + } + } + + private func sourceColor(_ source: NLOSSource) -> Color { + switch source { + case .live: return .green + case .replay: return .blue + case .synthetic: return .orange + } + } + + private func capabilityRow( + _ title: String, + _ availability: AppleCapabilityAvailability + ) -> some View { + HStack { + Text(title) + Spacer() + Label( + availability.rawValue.capitalized, + systemImage: availability == .available ? "checkmark.circle.fill" : "xmark.circle.fill" + ) + .foregroundStyle(availability == .available ? .green : .secondary) + } + .font(.subheadline) + } + + private func badge(_ text: String, color: Color) -> some View { + Text(text) + .font(.caption2.bold().monospaced()) + .lineLimit(1) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(color.opacity(0.14), in: Capsule()) + .foregroundStyle(color) + } + + private func card( + title: String, + @ViewBuilder content: () -> Content + ) -> some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + content() + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 18)) + } +} diff --git a/ui/ios-nlos/App/Info.plist b/ui/ios-nlos/App/Info.plist new file mode 100644 index 00000000..a2bfbecc --- /dev/null +++ b/ui/ios-nlos/App/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + RuView NLOS + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocalNetworkUsageDescription + RuView connects to an explicitly configured, authenticated NLOS processing server on your network. + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ui/ios-nlos/App/PrivacyInfo.xcprivacy b/ui/ios-nlos/App/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..e08a130b --- /dev/null +++ b/ui/ios-nlos/App/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + diff --git a/ui/ios-nlos/App/RuViewNLOSApp.swift b/ui/ios-nlos/App/RuViewNLOSApp.swift new file mode 100644 index 00000000..2ed04b24 --- /dev/null +++ b/ui/ios-nlos/App/RuViewNLOSApp.swift @@ -0,0 +1,12 @@ +import SwiftUI + +@main +struct RuViewNLOSApp: App { + @StateObject private var model = AppModel() + + var body: some Scene { + WindowGroup { + ContentView(model: model) + } + } +} diff --git a/ui/ios-nlos/App/TrackCanvas.swift b/ui/ios-nlos/App/TrackCanvas.swift new file mode 100644 index 00000000..aa0cd6e8 --- /dev/null +++ b/ui/ios-nlos/App/TrackCanvas.swift @@ -0,0 +1,81 @@ +import Foundation +import RuViewNLOSCore +import SwiftUI + +struct TrackCanvas: View { + let tracks: [NLOSTrack] + + var body: some View { + Canvas { context, size in + drawGrid(context: &context, size: size) + let radiusMeters = max( + 5, + min(100, tracks.flatMap { [abs($0.positionM.x), abs($0.positionM.z)] }.max() ?? 5) + ) + + for track in tracks { + let point = CGPoint( + x: size.width / 2 + CGFloat(track.positionM.x / radiusMeters) * size.width * 0.45, + y: size.height / 2 - CGFloat(track.positionM.z / radiusMeters) * size.height * 0.45 + ) + let uncertainty = min( + 34, + max(8, CGFloat(sqrt(max(track.covarianceDiagonalM2.x, track.covarianceDiagonalM2.z))) * 14) + ) + let color: Color = track.state == .degraded ? .orange : .cyan + let uncertaintyRect = CGRect( + x: point.x - uncertainty, + y: point.y - uncertainty, + width: uncertainty * 2, + height: uncertainty * 2 + ) + context.stroke( + Path(ellipseIn: uncertaintyRect), + with: .color(color.opacity(0.45)), + lineWidth: 1 + ) + context.fill( + Path(ellipseIn: CGRect(x: point.x - 5, y: point.y - 5, width: 10, height: 10)), + with: .color(color) + ) + context.draw( + Text(String(track.trackId.prefix(12))) + .font(.caption2.monospaced()) + .foregroundColor(.primary), + at: CGPoint(x: point.x, y: point.y + uncertainty + 10) + ) + } + } + .background(Color(uiColor: .secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(Color.secondary.opacity(0.25), lineWidth: 1) + } + .accessibilityHidden(true) + } + + private func drawGrid(context: inout GraphicsContext, size: CGSize) { + var path = Path() + for fraction in stride( + from: CGFloat(0.1), + through: CGFloat(0.9), + by: CGFloat(0.1) + ) { + let x = size.width * fraction + let y = size.height * fraction + path.move(to: CGPoint(x: x, y: 0)) + path.addLine(to: CGPoint(x: x, y: size.height)) + path.move(to: CGPoint(x: 0, y: y)) + path.addLine(to: CGPoint(x: size.width, y: y)) + } + context.stroke(path, with: .color(.secondary.opacity(0.12)), lineWidth: 0.5) + + var axes = Path() + axes.move(to: CGPoint(x: size.width / 2, y: 0)) + axes.addLine(to: CGPoint(x: size.width / 2, y: size.height)) + axes.move(to: CGPoint(x: 0, y: size.height / 2)) + axes.addLine(to: CGPoint(x: size.width, y: size.height / 2)) + context.stroke(axes, with: .color(.secondary.opacity(0.5)), lineWidth: 1) + } +} diff --git a/ui/ios-nlos/Package.swift b/ui/ios-nlos/Package.swift new file mode 100644 index 00000000..e7723c69 --- /dev/null +++ b/ui/ios-nlos/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "RuViewNLOS", + platforms: [ + .iOS(.v16), + .macOS(.v13), + ], + products: [ + .library(name: "RuViewNLOSCore", targets: ["RuViewNLOSCore"]), + .library(name: "RuViewNLOSApple", targets: ["RuViewNLOSApple"]), + ], + targets: [ + .target(name: "RuViewNLOSCore"), + .target( + name: "RuViewNLOSApple", + dependencies: ["RuViewNLOSCore"] + ), + .testTarget( + name: "RuViewNLOSCoreTests", + dependencies: ["RuViewNLOSCore"] + ), + .testTarget( + name: "RuViewNLOSAppleTests", + dependencies: ["RuViewNLOSApple"] + ), + ] +) diff --git a/ui/ios-nlos/README.md b/ui/ios-nlos/README.md new file mode 100644 index 00000000..b3613777 --- /dev/null +++ b/ui/ios-nlos/README.md @@ -0,0 +1,85 @@ +# RuView NLOS for iOS + +RuView NLOS is a native SwiftUI monitor for authenticated hidden target hypotheses produced by the RuView consumer time of flight pipeline. It is deliberately a display and transport adapter. It does not claim that Apple LiDAR or ARKit can perform optical non line of sight reconstruction. + +The package has two libraries: + +| Product | Responsibility | +|---|---| +| `RuViewNLOSCore` | Typed wire model, strict validation, freshness policy, sequence and replay guard, secure endpoint validation | +| `RuViewNLOSApple` | Apple capability probe, Keychain credential storage, authenticated WebSocket transport | + +`RuViewNLOS.xcodeproj` contains the directly buildable iOS SwiftUI app and links both local package products. + +## Evidence boundary + +The app consumes JSON envelopes with schema `ruview.nlos.track.v1`. A frame is displayed only after the following checks pass: + +1. The UTF 8 JSON frame is no larger than 256 KiB and has at most 16 unique tracks. +2. Every object has exactly the versioned keys. The session identifier, interoperable sequence, algorithm version, provenance strings, hashes, timestamps, vectors, covariance, confidence, entropy, signal quality, and modality contributions are bounded. +3. The expiry is after capture, no more than 5 seconds after capture, still in the future, and capture is no more than 1 second ahead of the local clock. +4. A connection binds to one session and each sequence must increase. A session change requires an explicit reconnect. +5. Live evidence must be at least `l1_measured`, must preserve a raw or compact normalized transient histogram, and cannot use replay transport. +6. `depth_only` provenance can never enter the live NLOS display path. +7. Synthetic evidence must be `l0_synthetic`, use the all zero calibration hash and replay transport, and receives a persistent `SYNTHETIC` watermark. +8. Tracks whose state is `unknown` are never displayed. A stale, malformed, oversized, replayed, or unsupported frame clears the entire current display. + +The client also schedules a local expiry for the last accepted frame. If the stream stalls without closing, the visualization is cleared at the envelope deadline. Decode and sequence processing run on a dedicated Swift actor, while the main actor receives only the newest bounded display frame. + +## Apple capability boundary + +The native probe reports these capabilities separately: + +| Apple signal | Public API status | NLOS interpretation | +|---|---|---| +| Scene depth | Probed with `ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth)` | Derived visible surface depth only | +| Smoothed scene depth | Probed with `.smoothedSceneDepth` | Derived visible surface depth only | +| Scene mesh | Probed with `supportsSceneReconstruction(.mesh)` | Visible environment geometry only | +| World pose | Probed with `ARWorldTrackingConfiguration.isSupported` | Motion and registration context only | +| Raw photon timing histograms | Reported unavailable | Required from an external supported transient sensor for this pipeline | + +The app never upgrades ARKit depth, mesh, or pose into optical NLOS evidence. The current monitor performs only static capability checks, does not start an `ARSession`, and does not request camera permission. + +## Security model + +Only `wss` endpoints are accepted. URLs containing embedded user credentials or fragments are rejected, and HTTP redirects are not followed. Pairing tokens must be 32 to 512 visible ASCII characters, are sent in the `Authorization: Bearer` header, and are never placed in the URL or frame body. + +On Apple platforms, the token is stored as a generic password with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. It is not written to `UserDefaults`, logs, source, fixtures, or crash messages. The URL session is ephemeral with cookies and caches disabled. A production endpoint needs a certificate trusted by iOS; this client does not bypass TLS validation or accept self signed certificates. + +The visualization is advisory. It must not directly trigger physical actuation or safety critical decisions. + +The app has no analytics or position telemetry and its privacy manifest declares no tracking or collected data. Track frames remain in memory only and are replaced by the newest valid frame. Leaving the active foreground disconnects the stream and clears track state; the visualization is also marked privacy sensitive for system snapshots. + +## Build and test + +Requirements: + +1. Swift 5.9 or newer for the package tests. +2. Xcode 15 or newer for the iOS app. +3. iOS 16 or newer for deployment. + +Run the deterministic protocol and security tests on macOS or Linux: + +```bash +cd ui/ios-nlos +swift test +``` + +Build the unsigned simulator app on macOS: + +```bash +cd ui/ios-nlos +xcodebuild \ + -project RuViewNLOS.xcodeproj \ + -scheme RuViewNLOS \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGNING_ALLOWED=NO \ + build +``` + +For a physical iPhone, open `RuViewNLOS.xcodeproj`, choose a development team and a unique bundle identifier, then build to the device. Enter an explicitly provisioned `wss` track endpoint and pairing token. Do not put the token in the endpoint query string. + +## Validation limits + +A successful Swift test or simulator build is software evidence only. It is not evidence that Apple hardware exposes photon timing histograms and it is not a reproduction of the MIT consumer NLOS result. Real hardware validation requires both an external supported time of flight sensor and captured RuView server output with reviewed calibration and provenance. The simulator normally reports ARKit sensor capabilities as unavailable. diff --git a/ui/ios-nlos/RuViewNLOS.xcodeproj/project.pbxproj b/ui/ios-nlos/RuViewNLOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ec3c6680 --- /dev/null +++ b/ui/ios-nlos/RuViewNLOS.xcodeproj/project.pbxproj @@ -0,0 +1,357 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + A00000000000000000000001 /* RuViewNLOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000001 /* RuViewNLOSApp.swift */; }; + A00000000000000000000002 /* AppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000002 /* AppModel.swift */; }; + A00000000000000000000003 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000003 /* ContentView.swift */; }; + A00000000000000000000004 /* TrackCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000004 /* TrackCanvas.swift */; }; + A00000000000000000000005 /* RuViewNLOSCore in Frameworks */ = {isa = PBXBuildFile; productRef = J00000000000000000000001 /* RuViewNLOSCore */; }; + A00000000000000000000006 /* RuViewNLOSApple in Frameworks */ = {isa = PBXBuildFile; productRef = J00000000000000000000002 /* RuViewNLOSApple */; }; + A00000000000000000000007 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B00000000000000000000007 /* PrivacyInfo.xcprivacy */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + B00000000000000000000001 /* RuViewNLOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuViewNLOSApp.swift; sourceTree = ""; }; + B00000000000000000000002 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = ""; }; + B00000000000000000000003 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + B00000000000000000000004 /* TrackCanvas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackCanvas.swift; sourceTree = ""; }; + B00000000000000000000005 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B00000000000000000000006 /* RuViewNLOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RuViewNLOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B00000000000000000000007 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D00000000000000000000002 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A00000000000000000000005 /* RuViewNLOSCore in Frameworks */, + A00000000000000000000006 /* RuViewNLOSApple in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C00000000000000000000000 = { + isa = PBXGroup; + children = ( + C00000000000000000000001 /* App */, + C00000000000000000000002 /* Products */, + ); + sourceTree = ""; + }; + C00000000000000000000001 /* App */ = { + isa = PBXGroup; + children = ( + B00000000000000000000001 /* RuViewNLOSApp.swift */, + B00000000000000000000002 /* AppModel.swift */, + B00000000000000000000003 /* ContentView.swift */, + B00000000000000000000004 /* TrackCanvas.swift */, + B00000000000000000000005 /* Info.plist */, + B00000000000000000000007 /* PrivacyInfo.xcprivacy */, + ); + path = App; + sourceTree = ""; + }; + C00000000000000000000002 /* Products */ = { + isa = PBXGroup; + children = ( + B00000000000000000000006 /* RuViewNLOS.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E00000000000000000000001 /* RuViewNLOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = H00000000000000000000001 /* Build configuration list for PBXNativeTarget "RuViewNLOS" */; + buildPhases = ( + D00000000000000000000001 /* Sources */, + D00000000000000000000002 /* Frameworks */, + D00000000000000000000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RuViewNLOS; + packageProductDependencies = ( + J00000000000000000000001 /* RuViewNLOSCore */, + J00000000000000000000002 /* RuViewNLOSApple */, + ); + productName = RuViewNLOS; + productReference = B00000000000000000000006 /* RuViewNLOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F00000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + E00000000000000000000001 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = H00000000000000000000002 /* Build configuration list for PBXProject "RuViewNLOS" */; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C00000000000000000000000; + packageReferences = ( + I00000000000000000000001 /* XCLocalSwiftPackageReference "." */, + ); + productRefGroup = C00000000000000000000002 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E00000000000000000000001 /* RuViewNLOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D00000000000000000000003 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A00000000000000000000007 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D00000000000000000000001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A00000000000000000000001 /* RuViewNLOSApp.swift in Sources */, + A00000000000000000000002 /* AppModel.swift in Sources */, + A00000000000000000000003 /* ContentView.swift in Sources */, + A00000000000000000000004 /* TrackCanvas.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + G00000000000000000000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.ruvnet.RuViewNLOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + G00000000000000000000002 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.ruvnet.RuViewNLOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + G00000000000000000000003 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + G00000000000000000000004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + H00000000000000000000001 /* Build configuration list for PBXNativeTarget "RuViewNLOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + G00000000000000000000001 /* Debug */, + G00000000000000000000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + H00000000000000000000002 /* Build configuration list for PBXProject "RuViewNLOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + G00000000000000000000003 /* Debug */, + G00000000000000000000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + I00000000000000000000001 /* XCLocalSwiftPackageReference "." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = .; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + J00000000000000000000001 /* RuViewNLOSCore */ = { + isa = XCSwiftPackageProductDependency; + package = I00000000000000000000001 /* XCLocalSwiftPackageReference "." */; + productName = RuViewNLOSCore; + }; + J00000000000000000000002 /* RuViewNLOSApple */ = { + isa = XCSwiftPackageProductDependency; + package = I00000000000000000000001 /* XCLocalSwiftPackageReference "." */; + productName = RuViewNLOSApple; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = F00000000000000000000001 /* Project object */; +} diff --git a/ui/ios-nlos/RuViewNLOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ui/ios-nlos/RuViewNLOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/ui/ios-nlos/RuViewNLOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ui/ios-nlos/RuViewNLOS.xcodeproj/xcshareddata/xcschemes/RuViewNLOS.xcscheme b/ui/ios-nlos/RuViewNLOS.xcodeproj/xcshareddata/xcschemes/RuViewNLOS.xcscheme new file mode 100644 index 00000000..5302df26 --- /dev/null +++ b/ui/ios-nlos/RuViewNLOS.xcodeproj/xcshareddata/xcschemes/RuViewNLOS.xcscheme @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/ios-nlos/Sources/RuViewNLOSApple/AppleCapabilityProbe.swift b/ui/ios-nlos/Sources/RuViewNLOSApple/AppleCapabilityProbe.swift new file mode 100644 index 00000000..d258c416 --- /dev/null +++ b/ui/ios-nlos/Sources/RuViewNLOSApple/AppleCapabilityProbe.swift @@ -0,0 +1,65 @@ +import Foundation +import RuViewNLOSCore + +#if canImport(ARKit) +import ARKit +#endif + +public enum AppleCapabilityAvailability: String, Equatable, Sendable { + case available + case unavailable +} + +public struct AppleNLOSCapabilityReport: Equatable, Sendable { + public let sceneDepth: AppleCapabilityAvailability + public let smoothedSceneDepth: AppleCapabilityAvailability + public let sceneMesh: AppleCapabilityAvailability + public let worldPose: AppleCapabilityAvailability + public let rawPhotonHistograms: AppleCapabilityAvailability + public let rawPhotonHistogramReason: String + + public init( + sceneDepth: AppleCapabilityAvailability, + smoothedSceneDepth: AppleCapabilityAvailability, + sceneMesh: AppleCapabilityAvailability, + worldPose: AppleCapabilityAvailability, + rawPhotonHistograms: AppleCapabilityAvailability, + rawPhotonHistogramReason: String + ) { + self.sceneDepth = sceneDepth + self.smoothedSceneDepth = smoothedSceneDepth + self.sceneMesh = sceneMesh + self.worldPose = worldPose + self.rawPhotonHistograms = rawPhotonHistograms + self.rawPhotonHistogramReason = rawPhotonHistogramReason + } +} + +public enum AppleCapabilityProbe { + public static func probe() -> AppleNLOSCapabilityReport { + #if canImport(ARKit) + let sceneDepth = ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) + let smoothedDepth = ARWorldTrackingConfiguration.supportsFrameSemantics(.smoothedSceneDepth) + let mesh = ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) + let pose = ARWorldTrackingConfiguration.isSupported + + return AppleNLOSCapabilityReport( + sceneDepth: sceneDepth ? .available : .unavailable, + smoothedSceneDepth: smoothedDepth ? .available : .unavailable, + sceneMesh: mesh ? .available : .unavailable, + worldPose: pose ? .available : .unavailable, + rawPhotonHistograms: .unavailable, + rawPhotonHistogramReason: "Public ARKit APIs expose derived depth, mesh, and pose, not the raw per-zone photon timing histograms required by this NLOS pipeline." + ) + #else + return AppleNLOSCapabilityReport( + sceneDepth: .unavailable, + smoothedSceneDepth: .unavailable, + sceneMesh: .unavailable, + worldPose: .unavailable, + rawPhotonHistograms: .unavailable, + rawPhotonHistogramReason: "ARKit is unavailable on this build host. Raw photon histograms are not exposed by public Apple APIs." + ) + #endif + } +} diff --git a/ui/ios-nlos/Sources/RuViewNLOSApple/KeychainPairingTokenStore.swift b/ui/ios-nlos/Sources/RuViewNLOSApple/KeychainPairingTokenStore.swift new file mode 100644 index 00000000..85b568ef --- /dev/null +++ b/ui/ios-nlos/Sources/RuViewNLOSApple/KeychainPairingTokenStore.swift @@ -0,0 +1,120 @@ +import Foundation +import RuViewNLOSCore + +#if canImport(Security) +import Security + +public enum PairingTokenStoreError: Error, LocalizedError, Sendable { + case keychainFailure(OSStatus) + case invalidStoredValue + + public var errorDescription: String? { + switch self { + case .keychainFailure: + return "The pairing token could not be accessed in Keychain." + case .invalidStoredValue: + return "The stored pairing token is invalid." + } + } +} + +public final class KeychainPairingTokenStore: @unchecked Sendable { + private let service: String + + public init(service: String = "org.ruvnet.RuViewNLOS.pairing") { + self.service = service + } + + public func save(_ token: String, for endpoint: URL) throws { + try WSSConnectionValidator.validatePairingToken(token) + let account = try WSSConnectionValidator.credentialAccount(for: endpoint) + guard let data = token.data(using: .utf8) else { + throw PairingTokenStoreError.invalidStoredValue + } + + let identity: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + ] + let attributes: [String: Any] = [ + kSecValueData as String: data, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + ] + + let updateStatus = SecItemUpdate(identity as CFDictionary, attributes as CFDictionary) + if updateStatus == errSecSuccess { return } + guard updateStatus == errSecItemNotFound else { + throw PairingTokenStoreError.keychainFailure(updateStatus) + } + + var insert = identity + attributes.forEach { insert[$0.key] = $0.value } + let insertStatus = SecItemAdd(insert as CFDictionary, nil) + guard insertStatus == errSecSuccess else { + throw PairingTokenStoreError.keychainFailure(insertStatus) + } + } + + public func load(for endpoint: URL) throws -> String? { + let account = try WSSConnectionValidator.credentialAccount(for: endpoint) + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne, + ] + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess, let data = item as? Data, + let token = String(data: data, encoding: .utf8) else { + if status == errSecSuccess { + throw PairingTokenStoreError.invalidStoredValue + } + throw PairingTokenStoreError.keychainFailure(status) + } + do { + try WSSConnectionValidator.validatePairingToken(token) + } catch { + throw PairingTokenStoreError.invalidStoredValue + } + return token + } + + public func deleteAll() throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + ] + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw PairingTokenStoreError.keychainFailure(status) + } + } +} + +#else + +public enum PairingTokenStoreError: Error, LocalizedError, Sendable { + case unavailable + + public var errorDescription: String? { + "Apple Keychain is unavailable on this platform." + } +} + +public final class KeychainPairingTokenStore: @unchecked Sendable { + public init(service: String = "org.ruvnet.RuViewNLOS.pairing") {} + + public func save(_ token: String, for endpoint: URL) throws { + throw PairingTokenStoreError.unavailable + } + public func load(for endpoint: URL) throws -> String? { + throw PairingTokenStoreError.unavailable + } + public func deleteAll() throws { throw PairingTokenStoreError.unavailable } +} + +#endif diff --git a/ui/ios-nlos/Sources/RuViewNLOSApple/NLOSWebSocketClient.swift b/ui/ios-nlos/Sources/RuViewNLOSApple/NLOSWebSocketClient.swift new file mode 100644 index 00000000..8c136083 --- /dev/null +++ b/ui/ios-nlos/Sources/RuViewNLOSApple/NLOSWebSocketClient.swift @@ -0,0 +1,284 @@ +import Foundation +import RuViewNLOSCore + +public enum NLOSStreamEvent: Sendable { + case connecting + case connected + case frame(TrackDisplayFrame) + case failClosed(String) + case disconnected(String) +} + +#if canImport(Darwin) + +private final class RejectRedirectDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable { + func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + completionHandler(nil) + } +} + +private enum ProcessedMessage: Sendable { + case authenticated(NLOSAuthenticatedSession) + case frame(TrackDisplayFrame) +} + +private actor FrameProcessor { + private let decoder = TrackEnvelopeDecoder() + private var streamGuard = TrackStreamGuard() + private var authenticatedSession: NLOSAuthenticatedSession? + + func process(_ data: Data, nowUnixMs: UInt64) throws -> ProcessedMessage { + guard let authenticatedSession else { + let authenticated = try decoder.decodeAuthenticated( + data, + nowUnixMs: nowUnixMs + ) + self.authenticatedSession = authenticated + return .authenticated(authenticated) + } + guard authenticatedSession.expiresAtUnixMs > nowUnixMs else { + throw NLOSValidationError.staleFrame + } + let envelope = try decoder.decode(data, nowUnixMs: nowUnixMs) + guard envelope.value.sessionId == authenticatedSession.sessionId else { + throw NLOSValidationError.sessionChanged + } + return .frame(try streamGuard.accept(envelope, nowUnixMs: nowUnixMs)) + } +} + +@MainActor +public final class NLOSWebSocketClient { + public var onEvent: ((NLOSStreamEvent) -> Void)? + + private var session: URLSession? + private var redirectDelegate: RejectRedirectDelegate? + private var socket: URLSessionWebSocketTask? + private var receiveTask: Task? + private var expiryTask: Task? + private var authenticationTask: Task? + private var sessionExpiryTask: Task? + private var connectionId: UUID? + + public init() {} + + deinit { + receiveTask?.cancel() + expiryTask?.cancel() + authenticationTask?.cancel() + sessionExpiryTask?.cancel() + socket?.cancel(with: .goingAway, reason: nil) + session?.invalidateAndCancel() + } + + public func connect(endpoint: URL, pairingToken: String) throws { + try WSSConnectionValidator.validate(endpoint: endpoint, pairingToken: pairingToken) + disconnect(emitEvent: false) + + let currentConnectionId = UUID() + let processor = FrameProcessor() + connectionId = currentConnectionId + + let configuration = URLSessionConfiguration.ephemeral + configuration.urlCache = nil + configuration.httpCookieStorage = nil + configuration.httpShouldSetCookies = false + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.timeoutIntervalForRequest = 15 + configuration.timeoutIntervalForResource = 86_400 + + let redirectDelegate = RejectRedirectDelegate() + let session = URLSession( + configuration: configuration, + delegate: redirectDelegate, + delegateQueue: nil + ) + var request = URLRequest(url: endpoint) + request.timeoutInterval = 15 + request.setValue("Bearer \(pairingToken)", forHTTPHeaderField: "Authorization") + request.setValue(TrackEnvelopeDecoder.schema, forHTTPHeaderField: "Sec-WebSocket-Protocol") + + let socket = session.webSocketTask(with: request) + socket.maximumMessageSize = TrackEnvelopeDecoder.maximumFrameBytes + self.session = session + self.redirectDelegate = redirectDelegate + self.socket = socket + onEvent?(.connecting) + socket.resume() + + authenticationTask = Task { [weak self] in + do { + try await Task.sleep(nanoseconds: 5_000_000_000) + } catch { + return + } + guard let self, self.connectionId == currentConnectionId else { return } + self.failClosed("Authentication acknowledgement timed out; all tracks were hidden.") + self.disconnect(emitEvent: true) + } + + receiveTask = Task { [weak self, weak socket] in + guard let socket else { return } + while !Task.isCancelled { + do { + let message = try await socket.receive() + guard let self, self.connectionId == currentConnectionId else { return } + await self.handle( + message, + connectionId: currentConnectionId, + processor: processor + ) + } catch { + guard !Task.isCancelled, let self, + self.connectionId == currentConnectionId else { return } + self.failClosed("Secure stream ended; all tracks were hidden.") + self.disconnect(emitEvent: true) + return + } + } + } + } + + public func disconnect() { + disconnect(emitEvent: true) + } + + private func disconnect(emitEvent: Bool) { + connectionId = nil + receiveTask?.cancel() + receiveTask = nil + expiryTask?.cancel() + expiryTask = nil + authenticationTask?.cancel() + authenticationTask = nil + sessionExpiryTask?.cancel() + sessionExpiryTask = nil + socket?.cancel(with: .normalClosure, reason: nil) + socket = nil + session?.invalidateAndCancel() + session = nil + redirectDelegate = nil + if emitEvent { + onEvent?(.disconnected("Disconnected; all tracks are hidden.")) + } + } + + private func handle( + _ message: URLSessionWebSocketTask.Message, + connectionId: UUID, + processor: FrameProcessor + ) async { + let data: Data + switch message { + case let .data(binary): + data = binary + case let .string(text): + guard let encoded = text.data(using: .utf8) else { + failClosed("A non UTF-8 frame was rejected.") + return + } + data = encoded + @unknown default: + failClosed("An unsupported WebSocket frame was rejected.") + return + } + + let nowUnixMs = Self.nowUnixMs() + do { + let processed = try await processor.process(data, nowUnixMs: nowUnixMs) + guard self.connectionId == connectionId else { return } + guard case let .frame(displayFrame) = processed else { + guard case let .authenticated(session) = processed else { return } + authenticationTask?.cancel() + authenticationTask = nil + scheduleSessionExpiry(session, connectionId: connectionId) + onEvent?(.connected) + return + } + guard displayFrame.expiresAtUnixMs > Self.nowUnixMs() else { + throw NLOSValidationError.staleFrame + } + onEvent?(.frame(displayFrame)) + scheduleExpiry(for: displayFrame, connectionId: connectionId) + } catch let validationError as NLOSValidationError { + failClosed(validationError.localizedDescription) + } catch { + failClosed("Frame validation failed; all tracks were hidden.") + } + } + + private func scheduleExpiry(for frame: TrackDisplayFrame, connectionId: UUID) { + expiryTask?.cancel() + let now = Self.nowUnixMs() + let delayMs = frame.expiresAtUnixMs > now ? frame.expiresAtUnixMs - now : 0 + let sequence = frame.sequence + let sessionId = frame.sessionId + + expiryTask = Task { [weak self] in + do { + try await Task.sleep(nanoseconds: delayMs * 1_000_000) + } catch { + return + } + guard let self, self.connectionId == connectionId else { return } + self.onEvent?(.failClosed( + "Frame \(sessionId)#\(sequence) expired; all tracks were hidden." + )) + } + } + + private func scheduleSessionExpiry( + _ session: NLOSAuthenticatedSession, + connectionId: UUID + ) { + sessionExpiryTask?.cancel() + let now = Self.nowUnixMs() + let delayMs = session.expiresAtUnixMs > now ? session.expiresAtUnixMs - now : 0 + sessionExpiryTask = Task { [weak self] in + do { + try await Task.sleep(nanoseconds: delayMs * 1_000_000) + } catch { + return + } + guard let self, self.connectionId == connectionId else { return } + self.failClosed("Authenticated session expired; all tracks were hidden.") + self.disconnect(emitEvent: true) + } + } + + private func failClosed(_ reason: String) { + expiryTask?.cancel() + expiryTask = nil + onEvent?(.failClosed(reason)) + } + + private static func nowUnixMs() -> UInt64 { + UInt64(Date().timeIntervalSince1970 * 1_000) + } +} + +#else + +@MainActor +public final class NLOSWebSocketClient { + public var onEvent: ((NLOSStreamEvent) -> Void)? + + public init() {} + + public func connect(endpoint: URL, pairingToken: String) throws { + try WSSConnectionValidator.validate(endpoint: endpoint, pairingToken: pairingToken) + onEvent?(.failClosed("Apple URLSession WebSocket support is unavailable on this platform.")) + } + + public func disconnect() { + onEvent?(.disconnected("Disconnected; all tracks are hidden.")) + } +} + +#endif diff --git a/ui/ios-nlos/Sources/RuViewNLOSCore/TrackEnvelope.swift b/ui/ios-nlos/Sources/RuViewNLOSCore/TrackEnvelope.swift new file mode 100644 index 00000000..0276b8f0 --- /dev/null +++ b/ui/ios-nlos/Sources/RuViewNLOSCore/TrackEnvelope.swift @@ -0,0 +1,319 @@ +import Foundation + +private struct AnyCodingKey: CodingKey { + let stringValue: String + let intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + intValue = nil + } + + init?(intValue: Int) { + stringValue = String(intValue) + self.intValue = intValue + } +} + +private func requireExactKeys(_ decoder: Decoder, allowed: Set) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + let actual = Set(container.allKeys.map(\.stringValue)) + guard actual == allowed else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Object keys do not match ruview.nlos.track.v1." + ) + ) + } +} + +public enum NLOSSource: String, Decodable, Sendable { + case live + case replay + case synthetic +} + +public enum EvidenceLevel: String, Decodable, Sendable, Comparable { + case l0Synthetic = "l0_synthetic" + case l1Measured = "l1_measured" + case l2Calibrated = "l2_calibrated" + case l3Corroborated = "l3_corroborated" + + private var rank: Int { + switch self { + case .l0Synthetic: return 0 + case .l1Measured: return 1 + case .l2Calibrated: return 2 + case .l3Corroborated: return 3 + } + } + + public static func < (lhs: EvidenceLevel, rhs: EvidenceLevel) -> Bool { + lhs.rank < rhs.rank + } +} + +public enum TransientKind: String, Decodable, Sendable { + case rawHistogram = "raw_histogram" + case compactNormalizedHistogram = "compact_normalized_histogram" + case depthOnly = "depth_only" + case replay +} + +public enum NLOSTransport: String, Decodable, Sendable { + case usbSerial = "usb_serial" + case ruviewServer = "ruview_server" + case replay +} + +public enum NLOSTrackState: String, Decodable, Sendable { + case tracking + case degraded + case unknown +} + +public struct Vector3: Decodable, Equatable, Sendable { + public let x: Double + public let y: Double + public let z: Double + + private enum CodingKeys: String, CodingKey { + case x, y, z + } + + public init(from decoder: Decoder) throws { + try requireExactKeys(decoder, allowed: ["x", "y", "z"]) + let container = try decoder.container(keyedBy: CodingKeys.self) + x = try container.decode(Double.self, forKey: .x) + y = try container.decode(Double.self, forKey: .y) + z = try container.decode(Double.self, forKey: .z) + } +} + +public struct ModalityContributions: Decodable, Equatable, Sendable { + public let lidar: Double + public let csi: Double + + private enum CodingKeys: String, CodingKey { + case lidar, csi + } + + public init(from decoder: Decoder) throws { + try requireExactKeys(decoder, allowed: ["lidar", "csi"]) + let container = try decoder.container(keyedBy: CodingKeys.self) + lidar = try container.decode(Double.self, forKey: .lidar) + csi = try container.decode(Double.self, forKey: .csi) + } +} + +public struct NLOSProvenance: Decodable, Equatable, Sendable { + public let sensorId: String + public let sensorModel: String + public let firmwareVersion: String + public let transientKind: TransientKind + public let histogramPreserved: Bool + public let transport: NLOSTransport + + private enum CodingKeys: String, CodingKey { + case sensorId + case sensorModel + case firmwareVersion + case transientKind + case histogramPreserved + case transport + } + + public init(from decoder: Decoder) throws { + try requireExactKeys( + decoder, + allowed: [ + "sensorId", + "sensorModel", + "firmwareVersion", + "transientKind", + "histogramPreserved", + "transport", + ] + ) + let container = try decoder.container(keyedBy: CodingKeys.self) + sensorId = try container.decode(String.self, forKey: .sensorId) + sensorModel = try container.decode(String.self, forKey: .sensorModel) + firmwareVersion = try container.decode(String.self, forKey: .firmwareVersion) + transientKind = try container.decode(TransientKind.self, forKey: .transientKind) + histogramPreserved = try container.decode(Bool.self, forKey: .histogramPreserved) + transport = try container.decode(NLOSTransport.self, forKey: .transport) + } +} + +public struct NLOSTrack: Decodable, Equatable, Identifiable, Sendable { + public let trackId: String + public let state: NLOSTrackState + public let positionM: Vector3 + public let velocityMps: Vector3 + public let covarianceDiagonalM2: Vector3 + public let confidence: Double + public let posteriorEntropy: Double + public let signalQuality: Double + public let modalityContributions: ModalityContributions + + public var id: String { trackId } + + private enum CodingKeys: String, CodingKey { + case trackId + case state + case positionM + case velocityMps + case covarianceDiagonalM2 + case confidence + case posteriorEntropy + case signalQuality + case modalityContributions + } + + public init(from decoder: Decoder) throws { + try requireExactKeys( + decoder, + allowed: [ + "trackId", + "state", + "positionM", + "velocityMps", + "covarianceDiagonalM2", + "confidence", + "posteriorEntropy", + "signalQuality", + "modalityContributions", + ] + ) + let container = try decoder.container(keyedBy: CodingKeys.self) + trackId = try container.decode(String.self, forKey: .trackId) + state = try container.decode(NLOSTrackState.self, forKey: .state) + positionM = try container.decode(Vector3.self, forKey: .positionM) + velocityMps = try container.decode(Vector3.self, forKey: .velocityMps) + covarianceDiagonalM2 = try container.decode(Vector3.self, forKey: .covarianceDiagonalM2) + confidence = try container.decode(Double.self, forKey: .confidence) + posteriorEntropy = try container.decode(Double.self, forKey: .posteriorEntropy) + signalQuality = try container.decode(Double.self, forKey: .signalQuality) + modalityContributions = try container.decode( + ModalityContributions.self, + forKey: .modalityContributions + ) + } +} + +public struct NLOSTrackEnvelope: Decodable, Equatable, Sendable { + public let schema: String + public let sessionId: String + public let sequence: UInt64 + public let capturedAtUnixMs: UInt64 + public let expiresAtUnixMs: UInt64 + public let source: NLOSSource + public let evidenceLevel: EvidenceLevel + public let algorithmVersion: String + public let calibrationHash: String + public let provenance: NLOSProvenance + public let tracks: [NLOSTrack] + + private enum CodingKeys: String, CodingKey { + case schema + case sessionId + case sequence + case capturedAtUnixMs + case expiresAtUnixMs + case source + case evidenceLevel + case algorithmVersion + case calibrationHash + case provenance + case tracks + } + + public init(from decoder: Decoder) throws { + try requireExactKeys( + decoder, + allowed: [ + "schema", + "sessionId", + "sequence", + "capturedAtUnixMs", + "expiresAtUnixMs", + "source", + "evidenceLevel", + "algorithmVersion", + "calibrationHash", + "provenance", + "tracks", + ] + ) + let container = try decoder.container(keyedBy: CodingKeys.self) + schema = try container.decode(String.self, forKey: .schema) + sessionId = try container.decode(String.self, forKey: .sessionId) + sequence = try container.decode(UInt64.self, forKey: .sequence) + capturedAtUnixMs = try container.decode(UInt64.self, forKey: .capturedAtUnixMs) + expiresAtUnixMs = try container.decode(UInt64.self, forKey: .expiresAtUnixMs) + source = try container.decode(NLOSSource.self, forKey: .source) + evidenceLevel = try container.decode(EvidenceLevel.self, forKey: .evidenceLevel) + algorithmVersion = try container.decode(String.self, forKey: .algorithmVersion) + calibrationHash = try container.decode(String.self, forKey: .calibrationHash) + provenance = try container.decode(NLOSProvenance.self, forKey: .provenance) + tracks = try container.decode([NLOSTrack].self, forKey: .tracks) + } +} + +/// First server message on every authenticated WebSocket connection. +public struct NLOSAuthenticatedSession: Decodable, Equatable, Sendable { + public let schema: String + public let sessionId: String + public let expiresAtUnixMs: UInt64 + + private enum CodingKeys: String, CodingKey { + case schema + case sessionId + case expiresAtUnixMs + } + + public init(from decoder: Decoder) throws { + try requireExactKeys( + decoder, + allowed: ["schema", "sessionId", "expiresAtUnixMs"] + ) + let container = try decoder.container(keyedBy: CodingKeys.self) + schema = try container.decode(String.self, forKey: .schema) + sessionId = try container.decode(String.self, forKey: .sessionId) + expiresAtUnixMs = try container.decode(UInt64.self, forKey: .expiresAtUnixMs) + } +} + +public struct ValidatedTrackEnvelope: Equatable, Sendable { + public let value: NLOSTrackEnvelope + + public var visibleTracks: [NLOSTrack] { + value.tracks.filter { $0.state != .unknown } + } + + public var watermark: String? { + switch value.source { + case .synthetic: return "SYNTHETIC" + case .replay: return "REPLAY" + case .live: return nil + } + } + + public func isFresh(atUnixMs nowUnixMs: UInt64) -> Bool { + value.expiresAtUnixMs > nowUnixMs + } +} + +public struct TrackDisplayFrame: Equatable, Sendable { + public let sessionId: String + public let sequence: UInt64 + public let capturedAtUnixMs: UInt64 + public let expiresAtUnixMs: UInt64 + public let source: NLOSSource + public let evidenceLevel: EvidenceLevel + public let algorithmVersion: String + public let provenance: NLOSProvenance + public let tracks: [NLOSTrack] + public let watermark: String? +} diff --git a/ui/ios-nlos/Sources/RuViewNLOSCore/TrackEnvelopeDecoder.swift b/ui/ios-nlos/Sources/RuViewNLOSCore/TrackEnvelopeDecoder.swift new file mode 100644 index 00000000..ef1848f0 --- /dev/null +++ b/ui/ios-nlos/Sources/RuViewNLOSCore/TrackEnvelopeDecoder.swift @@ -0,0 +1,282 @@ +import Foundation + +public enum NLOSValidationError: Error, Equatable, LocalizedError, Sendable { + case frameTooLarge(actualBytes: Int, maximumBytes: Int) + case malformedEnvelope + case invalidField(String) + case staleFrame + case futureDatedFrame + case excessiveLifetime + case replayedSequence + case sessionChanged + case insecureEndpoint + case invalidPairingToken + + public var errorDescription: String? { + switch self { + case let .frameTooLarge(actualBytes, maximumBytes): + return "Frame is \(actualBytes) bytes; the limit is \(maximumBytes) bytes." + case .malformedEnvelope: + return "Frame is not a valid ruview.nlos.track.v1 envelope." + case let .invalidField(field): + return "Frame failed validation for \(field)." + case .staleFrame: + return "Frame is stale and was hidden." + case .futureDatedFrame: + return "Frame timestamp is outside the allowed clock skew." + case .excessiveLifetime: + return "Frame lifetime exceeds the 5 second safety window." + case .replayedSequence: + return "Frame sequence was repeated or moved backwards." + case .sessionChanged: + return "Stream session changed without reconnecting." + case .insecureEndpoint: + return "Only a bounded wss endpoint without embedded credentials is allowed." + case .invalidPairingToken: + return "Pairing token must be 32 to 512 visible ASCII characters." + } + } +} + +public struct TrackEnvelopeDecoder: Sendable { + public static let schema = "ruview.nlos.track.v1" + public static let authenticatedSchema = "ruview.nlos.authenticated.v1" + public static let maximumFrameBytes = 256 * 1024 + public static let maximumTracks = 16 + public static let maximumLifetimeMs: UInt64 = 5_000 + public static let maximumFutureSkewMs: UInt64 = 1_000 + public static let maximumAuthenticationLifetimeMs: UInt64 = 60 * 60 * 1_000 + public static let maximumInteroperableSequence: UInt64 = 9_007_199_254_740_991 + + public init() {} + + public func decodeAuthenticated( + _ data: Data, + nowUnixMs: UInt64 + ) throws -> NLOSAuthenticatedSession { + guard data.count <= Self.maximumFrameBytes else { + throw NLOSValidationError.frameTooLarge( + actualBytes: data.count, + maximumBytes: Self.maximumFrameBytes + ) + } + let message: NLOSAuthenticatedSession + do { + message = try JSONDecoder().decode(NLOSAuthenticatedSession.self, from: data) + } catch { + throw NLOSValidationError.malformedEnvelope + } + guard message.schema == Self.authenticatedSchema else { + throw NLOSValidationError.invalidField("authenticated.schema") + } + guard isSafeIdentifier(message.sessionId, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("authenticated.sessionId") + } + guard message.expiresAtUnixMs > nowUnixMs else { + throw NLOSValidationError.staleFrame + } + guard message.expiresAtUnixMs <= Self.maximumInteroperableSequence else { + throw NLOSValidationError.invalidField("authenticated.expiresAtUnixMs") + } + guard message.expiresAtUnixMs - nowUnixMs + <= Self.maximumAuthenticationLifetimeMs + Self.maximumFutureSkewMs else { + throw NLOSValidationError.excessiveLifetime + } + return message + } + + public func decode(_ data: Data, nowUnixMs: UInt64) throws -> ValidatedTrackEnvelope { + guard data.count <= Self.maximumFrameBytes else { + throw NLOSValidationError.frameTooLarge( + actualBytes: data.count, + maximumBytes: Self.maximumFrameBytes + ) + } + + let envelope: NLOSTrackEnvelope + do { + envelope = try JSONDecoder().decode(NLOSTrackEnvelope.self, from: data) + } catch { + throw NLOSValidationError.malformedEnvelope + } + + try validate(envelope, nowUnixMs: nowUnixMs) + return ValidatedTrackEnvelope(value: envelope) + } + + private func validate(_ envelope: NLOSTrackEnvelope, nowUnixMs: UInt64) throws { + guard envelope.schema == Self.schema else { + throw NLOSValidationError.invalidField("schema") + } + guard isSafeIdentifier(envelope.sessionId, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("sessionId") + } + guard envelope.sequence <= Self.maximumInteroperableSequence else { + throw NLOSValidationError.invalidField("sequence") + } + guard envelope.capturedAtUnixMs <= Self.maximumInteroperableSequence, + envelope.expiresAtUnixMs <= Self.maximumInteroperableSequence else { + throw NLOSValidationError.invalidField("timestamp") + } + guard isSafeIdentifier(envelope.algorithmVersion, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("algorithmVersion") + } + guard isLowercaseSHA256(envelope.calibrationHash) else { + throw NLOSValidationError.invalidField("calibrationHash") + } + guard envelope.evidenceLevel != .l3Corroborated else { + throw NLOSValidationError.invalidField("evidenceLevel") + } + + let zeroHash = String(repeating: "0", count: 64) + if envelope.source == .synthetic { + guard envelope.evidenceLevel == .l0Synthetic else { + throw NLOSValidationError.invalidField("evidenceLevel") + } + guard envelope.calibrationHash == zeroHash else { + throw NLOSValidationError.invalidField("calibrationHash") + } + guard envelope.provenance.transport == .replay else { + throw NLOSValidationError.invalidField("provenance.transport") + } + guard envelope.provenance.transientKind == .replay else { + throw NLOSValidationError.invalidField("provenance.transientKind") + } + } else if envelope.evidenceLevel >= .l2Calibrated, + envelope.calibrationHash == zeroHash { + throw NLOSValidationError.invalidField("calibrationHash") + } + + guard envelope.expiresAtUnixMs > envelope.capturedAtUnixMs else { + throw NLOSValidationError.invalidField("expiresAtUnixMs") + } + guard envelope.expiresAtUnixMs - envelope.capturedAtUnixMs <= Self.maximumLifetimeMs else { + throw NLOSValidationError.excessiveLifetime + } + guard envelope.expiresAtUnixMs > nowUnixMs else { + throw NLOSValidationError.staleFrame + } + if envelope.capturedAtUnixMs > nowUnixMs { + guard envelope.capturedAtUnixMs - nowUnixMs <= Self.maximumFutureSkewMs else { + throw NLOSValidationError.futureDatedFrame + } + } + + try validate(envelope.provenance, source: envelope.source, evidence: envelope.evidenceLevel) + + guard envelope.tracks.count <= Self.maximumTracks else { + throw NLOSValidationError.invalidField("tracks") + } + var trackIds = Set() + for track in envelope.tracks { + try validate(track) + guard trackIds.insert(track.trackId).inserted else { + throw NLOSValidationError.invalidField("tracks.trackId") + } + } + } + + private func validate( + _ provenance: NLOSProvenance, + source: NLOSSource, + evidence: EvidenceLevel + ) throws { + guard isSafeIdentifier(provenance.sensorId, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("provenance.sensorId") + } + guard isSafeIdentifier(provenance.sensorModel, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("provenance.sensorModel") + } + guard isSafeIdentifier(provenance.firmwareVersion, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("provenance.firmwareVersion") + } + + if source == .live { + guard evidence >= .l1Measured else { + throw NLOSValidationError.invalidField("evidenceLevel") + } + let isLiveHistogram = provenance.transientKind == .rawHistogram + || provenance.transientKind == .compactNormalizedHistogram + guard provenance.histogramPreserved, + isLiveHistogram, + provenance.transport != .replay else { + throw NLOSValidationError.invalidField("provenance.transientKind") + } + } else if source == .replay { + guard provenance.transport == .replay, + provenance.transientKind == .replay, + provenance.histogramPreserved else { + throw NLOSValidationError.invalidField("provenance.transientKind") + } + } + } + + private func validate(_ track: NLOSTrack) throws { + guard isSafeIdentifier(track.trackId, maximumBytes: 64) else { + throw NLOSValidationError.invalidField("tracks.trackId") + } + guard isFinite(track.positionM, absoluteMaximum: 100) else { + throw NLOSValidationError.invalidField("tracks.positionM") + } + guard isFinite(track.velocityMps, absoluteMaximum: 20) else { + throw NLOSValidationError.invalidField("tracks.velocityMps") + } + guard isFiniteNonnegative(track.covarianceDiagonalM2, maximum: 10) else { + throw NLOSValidationError.invalidField("tracks.covarianceDiagonalM2") + } + guard isUnitInterval(track.confidence) else { + throw NLOSValidationError.invalidField("tracks.confidence") + } + guard track.posteriorEntropy.isFinite, track.posteriorEntropy >= 0 else { + throw NLOSValidationError.invalidField("tracks.posteriorEntropy") + } + guard isUnitInterval(track.signalQuality) else { + throw NLOSValidationError.invalidField("tracks.signalQuality") + } + guard isUnitInterval(track.modalityContributions.lidar), + isUnitInterval(track.modalityContributions.csi) else { + throw NLOSValidationError.invalidField("tracks.modalityContributions") + } + let contributionSum = track.modalityContributions.lidar + + track.modalityContributions.csi + guard contributionSum >= 0.999, contributionSum <= 1.001 else { + throw NLOSValidationError.invalidField("tracks.modalityContributions") + } + } + + private func isUnitInterval(_ value: Double) -> Bool { + value.isFinite && value >= 0 && value <= 1 + } + + private func isFinite(_ vector: Vector3, absoluteMaximum: Double) -> Bool { + [vector.x, vector.y, vector.z].allSatisfy { + $0.isFinite && abs($0) <= absoluteMaximum + } + } + + private func isFiniteNonnegative(_ vector: Vector3, maximum: Double) -> Bool { + [vector.x, vector.y, vector.z].allSatisfy { + $0.isFinite && $0 >= 0 && $0 <= maximum + } + } + + private func isSafeIdentifier(_ value: String, maximumBytes: Int) -> Bool { + guard !value.isEmpty, value.utf8.count <= maximumBytes else { return false } + return value.unicodeScalars.allSatisfy { scalar in + let code = scalar.value + return (code >= 48 && code <= 57) + || (code >= 65 && code <= 90) + || (code >= 97 && code <= 122) + || code == 45 + || code == 46 + || code == 58 + || code == 95 + } + } + + private func isLowercaseSHA256(_ value: String) -> Bool { + value.utf8.count == 64 && value.utf8.allSatisfy { byte in + (byte >= 48 && byte <= 57) || (byte >= 97 && byte <= 102) + } + } +} diff --git a/ui/ios-nlos/Sources/RuViewNLOSCore/TrackStreamGuard.swift b/ui/ios-nlos/Sources/RuViewNLOSCore/TrackStreamGuard.swift new file mode 100644 index 00000000..195e1594 --- /dev/null +++ b/ui/ios-nlos/Sources/RuViewNLOSCore/TrackStreamGuard.swift @@ -0,0 +1,87 @@ +import Foundation + +public struct TrackStreamGuard: Sendable { + private var boundSessionId: String? + private var lastSequence: UInt64? + + public init() {} + + public mutating func resetForReconnect() { + boundSessionId = nil + lastSequence = nil + } + + public mutating func accept( + _ envelope: ValidatedTrackEnvelope, + nowUnixMs: UInt64 + ) throws -> TrackDisplayFrame { + guard envelope.isFresh(atUnixMs: nowUnixMs) else { + throw NLOSValidationError.staleFrame + } + + let value = envelope.value + if let boundSessionId { + guard value.sessionId == boundSessionId else { + throw NLOSValidationError.sessionChanged + } + } + if let lastSequence { + guard value.sequence > lastSequence else { + throw NLOSValidationError.replayedSequence + } + } + + boundSessionId = value.sessionId + lastSequence = value.sequence + + return TrackDisplayFrame( + sessionId: value.sessionId, + sequence: value.sequence, + capturedAtUnixMs: value.capturedAtUnixMs, + expiresAtUnixMs: value.expiresAtUnixMs, + source: value.source, + evidenceLevel: value.evidenceLevel, + algorithmVersion: value.algorithmVersion, + provenance: value.provenance, + tracks: envelope.visibleTracks, + watermark: envelope.watermark + ) + } +} + +public enum WSSConnectionValidator { + public static func validate(endpoint: URL, pairingToken: String) throws { + _ = try credentialAccount(for: endpoint) + try validatePairingToken(pairingToken) + } + + /// Return a normalized Keychain account only for the exact NLOS socket + /// endpoint. Credentials are thereby bound to one authority. + public static func credentialAccount(for endpoint: URL) throws -> String { + let components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false) + guard endpoint.absoluteString.utf8.count <= 2_048, + endpoint.scheme?.lowercased() == "wss", + let host = endpoint.host, + !host.isEmpty, + endpoint.user == nil, + endpoint.password == nil, + endpoint.fragment == nil, + endpoint.path == "/api/v1/nlos/ws", + components?.percentEncodedQuery == nil else { + throw NLOSValidationError.insecureEndpoint + } + + if let port = endpoint.port, !(1...65_535).contains(port) { + throw NLOSValidationError.insecureEndpoint + } + + return "\(host.lowercased()):\(endpoint.port ?? 443)" + } + + public static func validatePairingToken(_ pairingToken: String) throws { + guard (32...512).contains(pairingToken.utf8.count), + pairingToken.utf8.allSatisfy({ $0 >= 0x21 && $0 <= 0x7e }) else { + throw NLOSValidationError.invalidPairingToken + } + } +} diff --git a/ui/ios-nlos/Tests/RuViewNLOSAppleTests/AppleCapabilityProbeTests.swift b/ui/ios-nlos/Tests/RuViewNLOSAppleTests/AppleCapabilityProbeTests.swift new file mode 100644 index 00000000..bc5a5a1c --- /dev/null +++ b/ui/ios-nlos/Tests/RuViewNLOSAppleTests/AppleCapabilityProbeTests.swift @@ -0,0 +1,31 @@ +import XCTest +@testable import RuViewNLOSApple + +final class AppleCapabilityProbeTests: XCTestCase { + func testRawPhotonHistogramsAreNeverClaimedByPublicAppleProbe() { + let report = AppleCapabilityProbe.probe() + + XCTAssertEqual(report.rawPhotonHistograms, .unavailable) + XCTAssertFalse(report.rawPhotonHistogramReason.isEmpty) + } + + #if !canImport(ARKit) + func testNonAppleBuildHostReportsARKitSignalsUnavailable() { + let report = AppleCapabilityProbe.probe() + + XCTAssertEqual(report.sceneDepth, .unavailable) + XCTAssertEqual(report.smoothedSceneDepth, .unavailable) + XCTAssertEqual(report.sceneMesh, .unavailable) + XCTAssertEqual(report.worldPose, .unavailable) + } + #endif + + #if !canImport(Security) + func testNonAppleBuildHostDoesNotFallBackToPlaintextTokenStorage() { + let store = KeychainPairingTokenStore() + + XCTAssertThrowsError(try store.save(String(repeating: "A", count: 32))) + XCTAssertThrowsError(try store.load()) + } + #endif +} diff --git a/ui/ios-nlos/Tests/RuViewNLOSCoreTests/TrackEnvelopeDecoderTests.swift b/ui/ios-nlos/Tests/RuViewNLOSCoreTests/TrackEnvelopeDecoderTests.swift new file mode 100644 index 00000000..83044d81 --- /dev/null +++ b/ui/ios-nlos/Tests/RuViewNLOSCoreTests/TrackEnvelopeDecoderTests.swift @@ -0,0 +1,525 @@ +import Foundation +import XCTest +@testable import RuViewNLOSCore + +final class TrackEnvelopeDecoderTests: XCTestCase { + private let nowUnixMs: UInt64 = 1_800_000_000_000 + private let decoder = TrackEnvelopeDecoder() + + func testValidLiveEnvelopeDecodes() throws { + let decoded = try decoder.decode(makeEnvelope(), nowUnixMs: nowUnixMs) + + XCTAssertEqual(decoded.value.schema, TrackEnvelopeDecoder.schema) + XCTAssertEqual(decoded.value.source, .live) + XCTAssertEqual(decoded.value.evidenceLevel, .l2Calibrated) + XCTAssertEqual(decoded.visibleTracks.map(\.trackId), ["target-1"]) + XCTAssertNil(decoded.watermark) + } + + func testAuthenticatedAcknowledgementUsesExactBoundedContract() throws { + let object: [String: Any] = [ + "schema": TrackEnvelopeDecoder.authenticatedSchema, + "sessionId": "session-a", + "expiresAtUnixMs": NSNumber(value: nowUnixMs + 60_000), + ] + let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) + let authenticated = try decoder.decodeAuthenticated(data, nowUnixMs: nowUnixMs) + XCTAssertEqual(authenticated.sessionId, "session-a") + + var unexpected = object + unexpected["extra"] = true + let malformed = try JSONSerialization.data( + withJSONObject: unexpected, + options: [.sortedKeys] + ) + XCTAssertThrowsError( + try decoder.decodeAuthenticated(malformed, nowUnixMs: nowUnixMs) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .malformedEnvelope) + } + + var excessive = object + excessive["expiresAtUnixMs"] = NSNumber( + value: nowUnixMs + TrackEnvelopeDecoder.maximumAuthenticationLifetimeMs + 1_001 + ) + let excessiveData = try JSONSerialization.data( + withJSONObject: excessive, + options: [.sortedKeys] + ) + XCTAssertThrowsError( + try decoder.decodeAuthenticated(excessiveData, nowUnixMs: nowUnixMs) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .excessiveLifetime) + } + } + + func testUnknownTrackIsAcceptedButNeverDisplayable() throws { + let decoded = try decoder.decode( + makeEnvelope(trackState: "unknown"), + nowUnixMs: nowUnixMs + ) + + XCTAssertEqual(decoded.value.tracks.count, 1) + XCTAssertTrue(decoded.visibleTracks.isEmpty) + } + + func testStaleFrameFailsClosed() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(capturedAtUnixMs: nowUnixMs - 2_000, expiresAtUnixMs: nowUnixMs), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .staleFrame) + } + } + + func testFutureFrameOutsideSkewFailsClosed() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + capturedAtUnixMs: nowUnixMs + 1_001, + expiresAtUnixMs: nowUnixMs + 2_000 + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .futureDatedFrame) + } + } + + func testLifetimeOverFiveSecondsIsRejected() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + capturedAtUnixMs: nowUnixMs - 100, + expiresAtUnixMs: nowUnixMs + 5_000 + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .excessiveLifetime) + } + } + + func testLiveDepthOnlyInputCannotBecomeNLOSEvidence() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(transientKind: "depth_only", histogramPreserved: false), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual( + error as? NLOSValidationError, + .invalidField("provenance.transientKind") + ) + } + } + + func testLiveFrameRequiresPreservedHistogram() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(histogramPreserved: false), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual( + error as? NLOSValidationError, + .invalidField("provenance.transientKind") + ) + } + } + + func testReplayTransientCannotBeRelabeledLive() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + transientKind: "replay", + histogramPreserved: true, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual( + error as? NLOSValidationError, + .invalidField("provenance.transientKind") + ) + } + } + + func testReplayTransportCannotBeRelabeledAsLiveHistogram() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + transientKind: "compact_normalized_histogram", + histogramPreserved: true, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual( + error as? NLOSValidationError, + .invalidField("provenance.transientKind") + ) + } + } + + func testSyntheticFrameRequiresL0AndCarriesWatermark() throws { + let zeroHash = String(repeating: "0", count: 64) + let decoded = try decoder.decode( + makeEnvelope( + source: "synthetic", + evidenceLevel: "l0_synthetic", + calibrationHash: zeroHash, + transientKind: "replay", + histogramPreserved: false, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + XCTAssertEqual(decoded.watermark, "SYNTHETIC") + XCTAssertNoThrow( + try decoder.decode( + makeEnvelope( + source: "synthetic", + evidenceLevel: "l0_synthetic", + calibrationHash: zeroHash, + transientKind: "replay", + histogramPreserved: true, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + ) + + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + source: "synthetic", + evidenceLevel: "l1_measured", + calibrationHash: zeroHash, + transientKind: "replay", + histogramPreserved: false, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("evidenceLevel")) + } + + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + source: "synthetic", + evidenceLevel: "l0_synthetic", + transientKind: "replay", + histogramPreserved: false, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("calibrationHash")) + } + + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope( + source: "synthetic", + evidenceLevel: "l0_synthetic", + calibrationHash: zeroHash, + transientKind: "replay", + histogramPreserved: false, + transport: "ruview_server" + ), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("provenance.transport")) + } + } + + func testCalibratedNonSyntheticFrameCannotUseZeroCalibrationHash() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(calibrationHash: String(repeating: "0", count: 64)), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("calibrationHash")) + } + } + + func testV1RejectsL3WithoutDualModalityLineage() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(evidenceLevel: "l3_corroborated"), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("evidenceLevel")) + } + } + + func testMeasuredUncalibratedReplayCanUseZeroCalibrationHash() throws { + let decoded = try decoder.decode( + makeEnvelope( + source: "replay", + evidenceLevel: "l1_measured", + calibrationHash: String(repeating: "0", count: 64), + transientKind: "replay", + histogramPreserved: true, + transport: "replay" + ), + nowUnixMs: nowUnixMs + ) + XCTAssertEqual(decoded.value.evidenceLevel, .l1Measured) + XCTAssertEqual(decoded.watermark, "REPLAY") + } + + func testOversizedFrameIsRejectedBeforeJSONParsing() { + let oversized = Data( + repeating: 0x20, + count: TrackEnvelopeDecoder.maximumFrameBytes + 1 + ) + + XCTAssertThrowsError(try decoder.decode(oversized, nowUnixMs: nowUnixMs)) { error in + XCTAssertEqual( + error as? NLOSValidationError, + .frameTooLarge( + actualBytes: TrackEnvelopeDecoder.maximumFrameBytes + 1, + maximumBytes: TrackEnvelopeDecoder.maximumFrameBytes + ) + ) + } + } + + func testPositionAndTrackCountBoundsAreEnforced() { + XCTAssertThrowsError( + try decoder.decode(makeEnvelope(positionX: 100.001), nowUnixMs: nowUnixMs) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("tracks.positionM")) + } + + let tracks = (0...TrackEnvelopeDecoder.maximumTracks).map { index in + makeTrack(trackId: "target-\(index)") + } + XCTAssertThrowsError( + try decoder.decode(makeEnvelope(tracks: tracks), nowUnixMs: nowUnixMs) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("tracks")) + } + } + + func testModalityContributionsMustSumToOne() { + var track = makeTrack(trackId: "target-1") + track["modalityContributions"] = ["lidar": 0.8, "csi": 0.8] + XCTAssertThrowsError( + try decoder.decode(makeEnvelope(tracks: [track]), nowUnixMs: nowUnixMs) + ) { error in + XCTAssertEqual( + error as? NLOSValidationError, + .invalidField("tracks.modalityContributions") + ) + } + } + + func testSequenceMustRemainInteroperableWithWebClients() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(sequence: TrackEnvelopeDecoder.maximumInteroperableSequence + 1), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("sequence")) + } + } + + func testUnknownObjectKeysAreRejected() { + XCTAssertThrowsError( + try decoder.decode( + makeEnvelope(includeUnknownRootKey: true), + nowUnixMs: nowUnixMs + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .malformedEnvelope) + } + } + + func testDuplicateTrackIdsAreRejected() { + let track = makeTrack(trackId: "duplicate") + XCTAssertThrowsError( + try decoder.decode(makeEnvelope(tracks: [track, track]), nowUnixMs: nowUnixMs) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidField("tracks.trackId")) + } + } + + func testStreamGuardRejectsReplayAndSessionSwitch() throws { + var guardrail = TrackStreamGuard() + let first = try decoder.decode( + makeEnvelope(sessionId: "session-a", sequence: 7), + nowUnixMs: nowUnixMs + ) + let repeated = try decoder.decode( + makeEnvelope(sessionId: "session-a", sequence: 7), + nowUnixMs: nowUnixMs + ) + let changedSession = try decoder.decode( + makeEnvelope(sessionId: "session-b", sequence: 8), + nowUnixMs: nowUnixMs + ) + + XCTAssertEqual(try guardrail.accept(first, nowUnixMs: nowUnixMs).sequence, 7) + XCTAssertThrowsError(try guardrail.accept(repeated, nowUnixMs: nowUnixMs)) { error in + XCTAssertEqual(error as? NLOSValidationError, .replayedSequence) + } + XCTAssertThrowsError(try guardrail.accept(changedSession, nowUnixMs: nowUnixMs)) { error in + XCTAssertEqual(error as? NLOSValidationError, .sessionChanged) + } + } + + func testReconnectAllowsNewSessionButRetainsMonotonicRule() throws { + var guardrail = TrackStreamGuard() + let first = try decoder.decode( + makeEnvelope(sessionId: "session-a", sequence: 99), + nowUnixMs: nowUnixMs + ) + let afterReconnect = try decoder.decode( + makeEnvelope(sessionId: "session-b", sequence: 0), + nowUnixMs: nowUnixMs + ) + + _ = try guardrail.accept(first, nowUnixMs: nowUnixMs) + guardrail.resetForReconnect() + XCTAssertEqual(try guardrail.accept(afterReconnect, nowUnixMs: nowUnixMs).sessionId, "session-b") + } + + func testWSSAndPairingTokenValidation() throws { + let validToken = String(repeating: "A", count: 32) + let secureURL = try XCTUnwrap(URL(string: "wss://127.0.0.1:9443/api/v1/nlos/ws")) + XCTAssertNoThrow( + try WSSConnectionValidator.validate(endpoint: secureURL, pairingToken: validToken) + ) + XCTAssertEqual( + try WSSConnectionValidator.credentialAccount(for: secureURL), + "127.0.0.1:9443" + ) + + let insecureURL = try XCTUnwrap(URL(string: "ws://127.0.0.1:9443/nlos")) + XCTAssertThrowsError( + try WSSConnectionValidator.validate(endpoint: insecureURL, pairingToken: validToken) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .insecureEndpoint) + } + + let embeddedCredentialURL = try XCTUnwrap(URL(string: "wss://user@example.test/nlos")) + XCTAssertThrowsError( + try WSSConnectionValidator.validate( + endpoint: embeddedCredentialURL, + pairingToken: validToken + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .insecureEndpoint) + } + + for unsafe in [ + "wss://example.test/nlos/v1/tracks", + "wss://example.test/api/v1/nlos/ws?token=secret", + "wss://example.test/api/v1/nlos/ws#fragment", + ] { + let endpoint = try XCTUnwrap(URL(string: unsafe)) + XCTAssertThrowsError( + try WSSConnectionValidator.validate(endpoint: endpoint, pairingToken: validToken) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .insecureEndpoint) + } + } + + XCTAssertThrowsError( + try WSSConnectionValidator.validate( + endpoint: secureURL, + pairingToken: "valid-looking-token\r\nInjected: yes" + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidPairingToken) + } + + XCTAssertThrowsError( + try WSSConnectionValidator.validate( + endpoint: secureURL, + pairingToken: String(repeating: "A", count: 31) + ) + ) { error in + XCTAssertEqual(error as? NLOSValidationError, .invalidPairingToken) + } + } + + private func makeEnvelope( + sessionId: String = "session-a", + sequence: UInt64 = 7, + source: String = "live", + evidenceLevel: String = "l2_calibrated", + calibrationHash: String = String(repeating: "a", count: 64), + transientKind: String = "raw_histogram", + histogramPreserved: Bool = true, + transport: String = "ruview_server", + trackState: String = "tracking", + positionX: Double = 1.25, + capturedAtUnixMs: UInt64? = nil, + expiresAtUnixMs: UInt64? = nil, + tracks: [[String: Any]]? = nil, + includeUnknownRootKey: Bool = false + ) -> Data { + let captured = capturedAtUnixMs ?? nowUnixMs - 100 + let expires = expiresAtUnixMs ?? nowUnixMs + 1_000 + let resolvedTracks = tracks ?? [ + makeTrack(trackId: "target-1", state: trackState, positionX: positionX), + ] + var object: [String: Any] = [ + "schema": TrackEnvelopeDecoder.schema, + "sessionId": sessionId, + "sequence": NSNumber(value: sequence), + "capturedAtUnixMs": NSNumber(value: captured), + "expiresAtUnixMs": NSNumber(value: expires), + "source": source, + "evidenceLevel": evidenceLevel, + "algorithmVersion": "consumer-nlos-0.1.0", + "calibrationHash": calibrationHash, + "provenance": [ + "sensorId": "spad-01", + "sensorModel": "VL53L8CH", + "firmwareVersion": "1.0.0", + "transientKind": transientKind, + "histogramPreserved": histogramPreserved, + "transport": transport, + ], + "tracks": resolvedTracks, + ] + if includeUnknownRootKey { + object["unexpected"] = true + } + return try! JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) + } + + private func makeTrack( + trackId: String, + state: String = "tracking", + positionX: Double = 1.25 + ) -> [String: Any] { + [ + "trackId": trackId, + "state": state, + "positionM": ["x": positionX, "y": 0.4, "z": -2.5], + "velocityMps": ["x": 0.1, "y": 0, "z": -0.2], + "covarianceDiagonalM2": ["x": 0.04, "y": 0.09, "z": 0.04], + "confidence": 0.88, + "posteriorEntropy": 0.32, + "signalQuality": 0.74, + "modalityContributions": ["lidar": 0.7, "csi": 0.3], + ] + } +} diff --git a/ui/mobile/README.md b/ui/mobile/README.md index 94379161..95b1cc36 100644 --- a/ui/mobile/README.md +++ b/ui/mobile/README.md @@ -7,6 +7,7 @@ WiFi-DensePose Mobile is a React Native / Expo companion app for the [WiFi-Dense > | Screen | What It Shows | > |--------|---------------| > | **Live** | 3D Gaussian splat body rendering with FPS counter, signal strength, confidence HUD | +> | **NLOS** | Authenticated hidden-target hypotheses with 2D plan / 3D perspective views, evidence provenance, freshness, and bounded synthetic replay | > | **Vitals** | Breathing rate (6-30 BPM) and heart rate (40-120 BPM) arc gauges with sparkline history | > | **Zones** | SVG floor plan with occupancy grid, zone legend, presence heatmap | > | **MAT** | Mass casualty assessment: survivor counter, triage alerts, zone management | @@ -29,6 +30,7 @@ npx expo start --web | | Feature | Details | |---|---------|---------| | **3D Live View** | Gaussian splat rendering | Three.js via WebView (native) or iframe (web), real-time pose overlay | +| **RuView NLOS Labs** | Hidden-target tracks | Authenticated `ruview.nlos.track.v1` frames, strict bounds, replay rejection, staleness, and visible provenance | | **Vital Signs** | Breathing + heart rate | Arc gauge components with sparkline 60-sample history, confidence indicators | | **Disaster Response** | WiFi-MAT dashboard | Survivor detection, START triage classification, priority alerts, zone scan tracking | | **Floor Plan** | SVG occupancy grid | Zone-level presence visualization, color-coded density, interactive legend | @@ -39,6 +41,15 @@ npx expo start --web | **Persistent State** | Zustand + AsyncStorage | Settings, connection preferences, and theme survive app restarts | | **Platform WiFi** | Native RSSI scanning | Android: `react-native-wifi-reborn`, iOS: stub (requires entitlement), Web: synthetic values | +### RuView NLOS on iOS and the web + +The NLOS tab is a cross-platform **track client**, not an iPhone LiDAR capture implementation. Apple Safari, Expo, and ordinary App Store APIs do not expose the raw photon timing histograms required by the research reconstruction pipeline. The client therefore accepts only: + +1. Live track frames from an authenticated RuView NLOS server session, after a transport-layer Bearer token is exchanged for a single-use WebSocket ticket. +2. Deterministic `SYNTHETIC` replay, always labeled `l0_synthetic` and always covered by a visible watermark. + +Unknown, expired, out-of-order, malformed, oversized, depth-only, or unauthenticated data is never presented as live NLOS. A native host can provide an ephemeral credential with `configureNlosBearerToken`, or an operator can paste a 32-to-512-character pairing credential into the masked NLOS screen input. The credential remains in memory, is sent only in the ticket request `Authorization` header, and is never persisted by this client. + --- ## Prerequisites @@ -133,6 +144,7 @@ ui/mobile/ websocket.ts WS path, reconnect delays, max attempts hooks/ usePoseStream.ts Subscribe to live or simulated sensing frames + useNlosStream.ts Authenticated NLOS / deterministic replay lifecycle useRssiScanner.ts Platform RSSI scanning hook useServerReachability.ts HTTP health check polling useTheme.ts Dark/light/system theme resolution @@ -148,6 +160,10 @@ ui/mobile/ GaussianSplatWebView.web.tsx Web iframe renderer LiveHUD.tsx FPS, RSSI, confidence, person count overlay useGaussianBridge.ts WebView message protocol + NLOSScreen/ + index.tsx NLOS evidence, controls, metrics, and safe fallback UI + HiddenTargetVisualization.tsx Memoized plan / perspective SVG renderer + ProvenancePanel.tsx Source, evidence, histogram, and freshness disclosure VitalsScreen/ index.tsx Breathing + heart rate dashboard BreathingGauge.tsx Arc gauge for breathing BPM @@ -172,6 +188,8 @@ ui/mobile/ ThemePicker.tsx Dark / light / system theme selector services/ ws.service.ts WebSocket client with auto-reconnect + simulation fallback + nlos.service.ts Bearer ticket exchange, bounded WebSocket, replay rejection + nlos.validation.ts Strict versioned NLOS track frame validation api.service.ts REST client (Axios) with retry logic rssi.service.ts Platform-agnostic RSSI scanner interface rssi.service.android.ts Android: react-native-wifi-reborn integration @@ -180,6 +198,7 @@ ui/mobile/ simulation.service.ts Generates synthetic SensingFrame data stores/ poseStore.ts Pose frames, connection status, frame history (Zustand) + nlosStore.ts NLOS frame ordering, provenance, rejection, and staleness matStore.ts MAT survivors, zones, alerts, disaster events (Zustand) settingsStore.ts Server URL, theme, RSSI toggle (Zustand + persist) theme/ @@ -190,6 +209,7 @@ ui/mobile/ index.ts Theme barrel export types/ sensing.ts SensingFrame, SensingNode, VitalsData, Classification + nlos.ts Canonical `ruview.nlos.track.v1` wire contract mat.ts Survivor, Alert, ScanZone, TriageStatus, DisasterType api.ts PoseStatus, ZoneConfig, HistoricalFrames, ApiError navigation.ts Navigation param lists @@ -249,6 +269,12 @@ The primary visualization screen. Renders a 3D Gaussian splat representation of Displays real-time breathing rate and heart rate extracted from CSI signal processing. Each vital sign is shown as an animated arc gauge (`GaugeArc` component) with the current BPM value, a 60-sample sparkline history (`SparklineChart`), and a confidence percentage. Normal ranges: breathing 6-30 BPM, heart rate 40-120 BPM. +### NLOS + +Displays hidden-target hypotheses produced upstream by RuView NLOS. The 2D plan and lightweight 3D perspective views render at most 16 tracks and covariance ellipses. The provenance card reports evidence level, transient kind, histogram preservation, sensor model, sequence, and freshness. Stale tracks remain visible only as muted historical context beneath a `STALE FRAME` overlay. + +The NLOS server URL is configured separately from the CSI socket. Live authentication uses `POST /api/v1/nlos/ws-ticket` with a Bearer credential; the response supplies a short-lived single-use `wss` URL. If no ephemeral credential is available, the tab starts in deterministic synthetic replay rather than silently relabeling simulated data as live. + ### Zones A floor plan view that maps WiFi sensing coverage to physical space. Uses SVG rendering (`react-native-svg`) to draw zones with color-coded occupancy density. The `useOccupancyGrid` hook computes grid cell values from incoming sensing frames. A legend shows the color scale from empty to high-density zones. @@ -259,8 +285,9 @@ Mass Casualty Assessment Tool for disaster response. Displays a survivor counter ### Settings -Configuration panel with four controls: +Configuration panel with separate sensing and NLOS controls: - **Server URL** — text input with URL validation; changes trigger WebSocket reconnect +- **RuView NLOS server URL** — separate base URL used only for the authenticated ticket exchange - **Theme** — dark / light / system picker - **RSSI Scanning** — toggle for platform-native WiFi RSSI scanning - **Alert Sound** — toggle for MAT alert audio notifications @@ -314,6 +341,22 @@ The REST client (`api.service.ts`) provides: All requests use Axios with a 5-second timeout and automatic retry (2 attempts). +### RuView NLOS protocol + +The NLOS client exchanges its in-memory Bearer credential at `POST /api/v1/nlos/ws-ticket`. The ticket response is capped at 8 KiB and must be exactly: + +```json +{ + "schema": "ruview.nlos.ws-ticket.v1", + "webSocketUrl": "wss://ruview.example/api/v1/nlos/ws?ticket=single-use", + "expiresAtUnixMs": 1770000000000 +} +``` + +The first WebSocket message must be `ruview.nlos.authenticated.v1`. Only then can the socket deliver `ruview.nlos.track.v1` frames for the same session. Track JSON is capped at 256 KiB and 16 tracks. Positions are bounded to ±100 m, velocity to ±20 m/s, covariance to 10 m², and expiration to five seconds. Sequence values must increase monotonically. + +Live frames require at least `l1_measured` evidence, preserved raw or compact normalized histograms, and `ruview_server` transport provenance. `depth_only` data cannot be labeled live NLOS. Synthetic frames require `l0_synthetic`, replay transport, the zero calibration hash, and the on-screen `SYNTHETIC` watermark. + --- @@ -332,10 +375,10 @@ Runs the Jest test suite via `jest-expo`. Tests cover: | Category | Files | What Is Tested | |----------|-------|----------------| | Components | 7 | `ConnectionBanner`, `GaugeArc`, `HudOverlay`, `OccupancyGrid`, `SignalBar`, `SparklineChart`, `StatusDot` | -| Screens | 5 | `LiveScreen`, `VitalsScreen`, `ZonesScreen`, `MATScreen`, `SettingsScreen` | -| Services | 4 | `ws.service`, `api.service`, `rssi.service`, `simulation.service` | -| Stores | 3 | `poseStore`, `matStore`, `settingsStore` | -| Hooks | 3 | `usePoseStream`, `useRssiScanner`, `useServerReachability` | +| Screens | 6 | Existing screens plus `NLOSScreen` | +| Services | 6 | Existing services plus NLOS transport and protocol validation | +| Stores | 4 | Existing stores plus NLOS ordering, provenance, and staleness state | +| Hooks | 4 | Existing hooks plus the NLOS authenticated/replay lifecycle | | Utils | 3 | `colorMap`, `ringBuffer`, `urlValidator` | ### End-to-End Tests (Maestro) diff --git a/ui/mobile/eslint.config.js b/ui/mobile/eslint.config.js new file mode 100644 index 00000000..db7937a5 --- /dev/null +++ b/ui/mobile/eslint.config.js @@ -0,0 +1,59 @@ +const eslint = require('@eslint/js'); +const typescriptParser = require('@typescript-eslint/parser'); +const typescriptPlugin = require('@typescript-eslint/eslint-plugin'); +const reactHooks = require('eslint-plugin-react-hooks'); +const globals = require('globals'); + +const sourceFiles = ['**/*.{js,jsx,ts,tsx}']; +const typescriptFiles = ['**/*.{ts,tsx}']; + +module.exports = [ + { + ignores: ['node_modules/**', 'dist/**', '.expo/**', 'coverage/**', 'src/assets/webview/**'], + }, + { + files: sourceFiles, + ...eslint.configs.recommended, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2025, + ...globals.jest, + ...globals.node, + }, + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + }, + plugins: { + 'react-hooks': reactHooks, + }, + rules: { + ...eslint.configs.recommended.rules, + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, + { + files: typescriptFiles, + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaFeatures: { jsx: true }, + project: './tsconfig.json', + }, + }, + plugins: { + '@typescript-eslint': typescriptPlugin, + }, + rules: { + ...typescriptPlugin.configs.recommended.rules, + 'no-undef': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-require-imports': 'off', + }, + }, +]; diff --git a/ui/mobile/jest.config.js b/ui/mobile/jest.config.js index a9351cba..3a39d3d7 100644 --- a/ui/mobile/jest.config.js +++ b/ui/mobile/jest.config.js @@ -7,7 +7,7 @@ module.exports = { ...(expoPreset.setupFiles || []), ], setupFilesAfterEnv: ['/jest.setup.ts'], - testPathIgnorePatterns: ['/node_modules/', '/__mocks__/'], + testPathIgnorePatterns: ['/node_modules/', '/__mocks__/', '/src/__tests__/test-utils.tsx'], transformIgnorePatterns: [ 'node_modules/(?!(expo|expo-.+|react-native|@react-native|react-native-webview|react-native-reanimated|react-native-svg|react-native-safe-area-context|react-native-screens|@react-navigation|@expo|@unimodules|expo-modules-core|react-native-worklets)/)', ], diff --git a/ui/mobile/jest.setup.ts b/ui/mobile/jest.setup.ts index 44de669e..33f41046 100644 --- a/ui/mobile/jest.setup.ts +++ b/ui/mobile/jest.setup.ts @@ -7,7 +7,7 @@ jest.mock('react-native-wifi-reborn', () => ({ })); jest.mock('react-native-reanimated', () => - require('react-native-reanimated/mock') + require('./src/__tests__/__mocks__/reanimated') ); jest.mock('react-native-webview', () => { diff --git a/ui/mobile/package-lock.json b/ui/mobile/package-lock.json index 683d69d8..b7e8a532 100644 --- a/ui/mobile/package-lock.json +++ b/ui/mobile/package-lock.json @@ -15,10 +15,10 @@ "@types/three": "^0.183.1", "axios": "^1.15.2", "expo": "~55.0.4", - "expo-status-bar": "~55.0.4", + "expo-status-bar": "~55.0.6", "react": "19.2.0", - "react-dom": "19.2.6", - "react-native": "0.85.2", + "react-dom": "19.2.0", + "react-native": "0.83.10", "react-native-gesture-handler": "~2.30.0", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.2", @@ -32,28 +32,31 @@ "zustand": "^5.0.12" }, "devDependencies": { + "@eslint/js": "10.0.1", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^13.3.3", - "@types/jest": "^30.0.0", + "@types/jest": "29.5.14", "@types/react": "~19.2.2", "@typescript-eslint/eslint-plugin": "^8.59.3", "@typescript-eslint/parser": "^8.56.1", "babel-preset-expo": "^55.0.10", "eslint": "^10.2.1", - "jest": "^30.2.0", + "eslint-plugin-react-hooks": "7.1.1", + "globals": "17.11.0", + "jest": "~29.7.0", "jest-expo": "^55.0.9", "prettier": "^3.8.3", - "react-native-worklets": "^0.7.4", + "react-native-worklets": "0.7.4", "typescript": "~5.9.2" } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -62,29 +65,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -110,13 +113,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -126,25 +129,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -163,17 +166,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -193,12 +196,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, @@ -219,9 +222,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -235,49 +238,49 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -287,35 +290,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -325,14 +328,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -342,79 +345,165 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -441,12 +530,12 @@ } }, "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", - "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -459,7 +548,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -472,7 +560,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -485,7 +572,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -498,7 +584,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -538,12 +623,12 @@ } }, "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", - "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -553,12 +638,12 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", - "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -571,7 +656,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -587,7 +671,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -600,7 +683,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -628,7 +710,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -653,7 +734,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -666,7 +746,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -679,7 +758,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -704,7 +782,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -720,7 +797,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -763,14 +839,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -780,14 +856,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -797,12 +873,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -812,13 +888,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -844,17 +920,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -864,13 +940,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -880,13 +956,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -911,13 +987,13 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-flow": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -927,13 +1003,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -943,14 +1019,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -960,12 +1036,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -975,12 +1051,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1006,13 +1082,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1022,12 +1098,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1037,12 +1113,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1052,16 +1128,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1071,12 +1147,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1086,13 +1162,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1102,12 +1178,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1117,13 +1193,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1133,14 +1209,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1199,12 +1275,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1214,12 +1290,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1245,12 +1321,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1260,13 +1336,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1304,13 +1380,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1320,12 +1396,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1433,50 +1509,31 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse--for-generate-function-map": { - "name": "@babel/traverse", - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -1484,13 +1541,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1521,40 +1578,6 @@ "node": ">=0.8.0" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1625,6 +1648,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", @@ -1649,6 +1693,124 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@expo/cli": { + "version": "55.0.35", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.35.tgz", + "integrity": "sha512-XEpAyU7UZNmDVThis8Aki/I9bqvTKYQwbcyCBQIGaJKdtNMOfjiymjLaHMJJKH8zzV/yCvA38A12JtLwp84Xpw==", + "license": "MIT", + "dependencies": { + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~55.0.20", + "@expo/config-plugins": "~55.0.11", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.1.3", + "@expo/image-utils": "^0.8.16", + "@expo/json-file": "^10.0.15", + "@expo/log-box": "55.0.13", + "@expo/metro": "~55.1.1", + "@expo/metro-config": "~55.0.26", + "@expo/osascript": "^2.4.4", + "@expo/package-manager": "^1.10.6", + "@expo/plist": "^0.5.4", + "@expo/prebuild-config": "^55.0.21", + "@expo/require-utils": "^55.0.7", + "@expo/router-server": "^55.0.19", + "@expo/schema-utils": "^55.0.5", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.4.0", + "@react-native/dev-middleware": "0.83.10", + "accepts": "^1.3.8", + "agent-cli-detector": "^0.1.2", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "dnssd-advertise": "^1.1.6", + "expo-server": "^55.0.12", + "fetch-nodeshim": "^0.4.10", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.2.1", + "multitars": "^1.0.2", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^4.0.3", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "terminal-link": "^2.1.1", + "toqr": "^0.1.1", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1", + "zod": "^3.25.76" + }, + "bin": { + "expo-internal": "build/bin/cli" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/cli/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1659,33 +1821,32 @@ } }, "node_modules/@expo/config": { - "version": "55.0.8", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.8.tgz", - "integrity": "sha512-D7RYYHfErCgEllGxNwdYdkgzLna7zkzUECBV3snbUpf7RvIpB5l1LpCgzuVoc5KVew5h7N1Tn4LnT/tBSUZsQg==", + "version": "55.0.20", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.20.tgz", + "integrity": "sha512-TnJEOtdHCin2rZ/OVEGo0fuT+CWz0VqhyGWz+zsARvKP45aP8LFQMzeHuWmOpfm7XLJcsMen7Y5gO+xkzcRXkg==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~55.0.6", - "@expo/config-types": "^55.0.5", - "@expo/json-file": "^10.0.12", - "@expo/require-utils": "^55.0.2", + "@expo/config-plugins": "~55.0.11", + "@expo/config-types": "^55.0.6", + "@expo/json-file": "^10.0.15", + "@expo/require-utils": "^55.0.7", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", - "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "node_modules/@expo/config-plugins": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.6.tgz", - "integrity": "sha512-cIox6FjZlFaaX40rbQ3DvP9e87S5X85H9uw+BAxJE5timkMhuByy3GAlOsj1h96EyzSiol7Q6YIGgY1Jiz4M+A==", + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.11.tgz", + "integrity": "sha512-85ZSmIK8rMfvYbG/2IHtwnykVdb6LI0ZavduM3ZwUMThyyVegYjVsO5Zek2ec8xzPhCmYX0ar5onnFEcwUDUlw==", "license": "MIT", "dependencies": { - "@expo/config-types": "^55.0.5", - "@expo/json-file": "~10.0.12", - "@expo/plist": "^0.5.2", + "@expo/config-types": "^55.0.6", + "@expo/json-file": "~10.0.15", + "@expo/plist": "^0.5.4", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", @@ -1698,10 +1859,29 @@ "xml2js": "0.6.0" } }, + "node_modules/@expo/config-plugins/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/config-plugins/node_modules/@expo/json-file": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz", + "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, "node_modules/@expo/config-types": { - "version": "55.0.5", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-55.0.5.tgz", - "integrity": "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==", + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-55.0.6.tgz", + "integrity": "sha512-S+GJKYoIjnWlert/9vXuTohaTsMbyOLSVxdIgPgoq3P4N1p4CWrfyZLnz6qRug8wYSO5fcYcS9mFleyEP8wRLg==", "license": "MIT" }, "node_modules/@expo/devcert": { @@ -1724,9 +1904,9 @@ } }, "node_modules/@expo/devtools": { - "version": "55.0.2", - "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-55.0.2.tgz", - "integrity": "sha512-4VsFn9MUriocyuhyA+ycJP3TJhUsOFHDc270l9h3LhNpXMf6wvIdGcA0QzXkZtORXmlDybWXRP2KT1k36HcQkA==", + "version": "55.0.3", + "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-55.0.3.tgz", + "integrity": "sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg==", "license": "MIT", "dependencies": { "chalk": "^4.1.2" @@ -1744,10 +1924,21 @@ } } }, + "node_modules/@expo/dom-webview": { + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/@expo/dom-webview/-/dom-webview-55.0.6.tgz", + "integrity": "sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/@expo/env": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.1.tgz", - "integrity": "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.3.tgz", + "integrity": "sha512-Rhu/lJ1kOhqzJvLwW0iB4WKUzB1nFa8WBwXFL44qkSTNwibjrbI8lA46Ix6W2MMTdlUqL1m85Gdyx0T7nGpREg==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -1759,12 +1950,12 @@ } }, "node_modules/@expo/fingerprint": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.16.5.tgz", - "integrity": "sha512-mLrcymtgkW9IJ/G1e8MH1Xt2VIb1MOS86ePY0ePcnV3nVyJqm7gfa/AXD1Hk+eZXvf8XhioYz6QZaamBdEzR3A==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.16.8.tgz", + "integrity": "sha512-RaLOikl+alkvG9ZrBQFGi3kVXJdG5GQXY1H3V1ZFCwFyhFikwuBozVrl4KL7U+N0Tm8Bf1qDmpTEIx8JOyrQog==", "license": "MIT", "dependencies": { - "@expo/env": "^2.0.11", + "@expo/env": "^2.1.3", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", @@ -1781,24 +1972,24 @@ } }, "node_modules/@expo/image-utils": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", - "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", + "version": "0.8.16", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.16.tgz", + "integrity": "sha512-43j9zfSDau82f2u0Wu8mi12FtTlHNvZT84WYwXuFEXputDHFk3kyebYhkjMg8ESNtWqhLGy+ppAh4SN7IcPcFg==", "license": "MIT", "dependencies": { + "@expo/require-utils": "^55.0.7", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", - "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "node_modules/@expo/json-file": { - "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz", + "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -1806,67 +1997,148 @@ } }, "node_modules/@expo/local-build-cache-provider": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.6.tgz", - "integrity": "sha512-4kfdv48sKzokijMqi07fINYA9/XprshmPgSLf8i69XgzIv2YdRyBbb70SzrufB7PDneFoltz8N83icW8gOOj1g==", + "version": "55.0.15", + "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.15.tgz", + "integrity": "sha512-coFNPt2gGmWtTJwqHlvx/Us9/VWK+pTHuP4jv/NM5jaHZuN0FhX7m2FvwthbluHUGpaVBc8+AxEDmDVqTfKtHg==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.8", + "@expo/config": "~55.0.19", "chalk": "^4.1.2" } }, - "node_modules/@expo/metro": { - "version": "54.2.0", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", - "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", + "node_modules/@expo/log-box": { + "version": "55.0.13", + "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.13.tgz", + "integrity": "sha512-pV623uwyKjw/L1HVWOpwWOu/ISLH1+c+ESVv30alQMbEaE3cLcwcQ+UnHiAGayMBNMQwK57eckOgH40RBXHfCA==", "license": "MIT", "dependencies": { - "metro": "0.83.3", - "metro-babel-transformer": "0.83.3", - "metro-cache": "0.83.3", - "metro-cache-key": "0.83.3", - "metro-config": "0.83.3", - "metro-core": "0.83.3", - "metro-file-map": "0.83.3", - "metro-minify-terser": "0.83.3", - "metro-resolver": "0.83.3", - "metro-runtime": "0.83.3", - "metro-source-map": "0.83.3", - "metro-symbolicate": "0.83.3", - "metro-transform-plugins": "0.83.3", - "metro-transform-worker": "0.83.3" + "@expo/dom-webview": "^55.0.6", + "anser": "^1.4.9", + "stacktrace-parser": "^0.1.10" + }, + "peerDependencies": { + "@expo/dom-webview": "^55.0.6", + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/metro": { + "version": "55.1.2", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-55.1.2.tgz", + "integrity": "sha512-EOBoqqX4jD7JvHBHKDoek8SmWyAFPuJePyDSqIAhIjOCPAM67fVxd4+pCXJ6SDK1P9so55v2iTFBwCoeCmEj4Q==", + "license": "MIT", + "dependencies": { + "metro": "0.83.8", + "metro-babel-transformer": "0.83.8", + "metro-cache": "0.83.8", + "metro-cache-key": "0.83.8", + "metro-config": "0.83.8", + "metro-core": "0.83.8", + "metro-file-map": "0.83.8", + "metro-minify-terser": "0.83.8", + "metro-resolver": "0.83.8", + "metro-runtime": "0.83.8", + "metro-source-map": "0.83.8", + "metro-symbolicate": "0.83.8", + "metro-transform-plugins": "0.83.8", + "metro-transform-worker": "0.83.8" + } + }, + "node_modules/@expo/metro-config": { + "version": "55.0.26", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.26.tgz", + "integrity": "sha512-XpnPiKVkqCZrGlASM3vFw3ik20GFjQYfa0N48aPEMQgQvWaX9e8Ete34NGjzROz+24nfFmgULXJZg4Z7YmAnaA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~55.0.20", + "@expo/env": "~2.1.3", + "@expo/json-file": "~10.0.15", + "@expo/metro": "~55.1.1", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.32.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "^8.5.14", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/@expo/json-file": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz", + "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/metro-config/node_modules/@expo/json-file/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" } }, "node_modules/@expo/osascript": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", - "integrity": "sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.1.tgz", + "integrity": "sha512-Zn03EX6In7ts2lPUW2ESUSkEhEWQN1qqsiXjadtZMJOuZRkMiAg1ZQHuvz9DjByDWNJ2pBwAGyrts9lj9k389g==", "license": "MIT", "dependencies": { - "@expo/spawn-async": "^1.7.2" + "@expo/spawn-async": "^1.8.0" }, "engines": { "node": ">=12" } }, "node_modules/@expo/package-manager": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.10.3.tgz", - "integrity": "sha512-ZuXiK/9fCrIuLjPSe1VYmfp0Sa85kCMwd8QQpgyi5ufppYKRtLBg14QOgUqj8ZMbJTxE0xqzd0XR7kOs3vAK9A==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.13.1.tgz", + "integrity": "sha512-y/K+CaYYpZpNGZhSX4HyLT/vyIunFjNfyoxNysPBCefeLKI/VCx6f9LNPzrxayr3rCYO5bl9O8H+HRQK265Nkg==", "license": "MIT", "dependencies": { - "@expo/json-file": "^10.0.12", - "@expo/spawn-async": "^1.7.2", + "@expo/json-file": "^11.0.1", + "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "resolve-workspace-root": "^2.0.0" } }, + "node_modules/@expo/package-manager/node_modules/@expo/json-file": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz", + "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, "node_modules/@expo/plist": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.2.tgz", - "integrity": "sha512-o4xdVdBpe4aTl3sPMZ2u3fJH4iG1I768EIRk1xRZP+GaFI93MaR3JvoFibYqxeTmLQ1p1kNEVqylfUjezxx45g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.4.tgz", + "integrity": "sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -1874,10 +2146,31 @@ "xmlbuilder": "^15.1.1" } }, + "node_modules/@expo/prebuild-config": { + "version": "55.0.21", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.21.tgz", + "integrity": "sha512-3OVPlg/iufrJ7dYjNwDwOXR2qX0h348xdPQTRuMfKVtcPlqpAH9fgz6CiSPKykGHyUanwgJRWiB7+XBjzzl6dg==", + "license": "MIT", + "dependencies": { + "@expo/config": "~55.0.20", + "@expo/config-plugins": "~55.0.11", + "@expo/config-types": "^55.0.6", + "@expo/image-utils": "^0.8.16", + "@expo/json-file": "^10.0.15", + "@react-native/normalize-colors": "0.83.10", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/@expo/require-utils": { - "version": "55.0.2", - "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.2.tgz", - "integrity": "sha512-dV5oCShQ1umKBKagMMT4B/N+SREsQe3lU4Zgmko5AO0rxKV0tynZT6xXs+e2JxuqT4Rz997atg7pki0BnZb4uw==", + "version": "55.0.7", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.7.tgz", + "integrity": "sha512-nkgOCNeWIVfLy3B+THeuqMGNKo0x6jBit9ofE09pHL7XHSkWcEnIYRCLgks8E3wj9q3ylcF1BQLrTxGhPja7iA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -1893,10 +2186,44 @@ } } }, + "node_modules/@expo/router-server": { + "version": "55.0.19", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.19.tgz", + "integrity": "sha512-bm2CFp9eI9biqZXz7Vkske0LXHZaLyonWV6sTEWUaR8zJ0NdVnUxb1Vd0iCdhW8GtweOUfMK4z7gojd0kydxjA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "@expo/metro-runtime": "^55.0.12", + "expo": "*", + "expo-constants": "^55.0.17", + "expo-font": "^55.0.8", + "expo-router": "*", + "expo-server": "^55.0.12", + "react": "*", + "react-dom": "*", + "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" + }, + "peerDependenciesMeta": { + "@expo/metro-runtime": { + "optional": true + }, + "expo-router": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, "node_modules/@expo/schema-utils": { - "version": "55.0.2", - "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-55.0.2.tgz", - "integrity": "sha512-QZ5WKbJOWkCrMq0/kfhV9ry8te/OaS34YgLVpG8u9y2gix96TlpRTbxM/YATjNcUR2s4fiQmPCOxkGtog4i37g==", + "version": "55.0.5", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-55.0.5.tgz", + "integrity": "sha512-wdV4SzWJ/l+6y/r9vDaqPqXGyfxUD1igbX8rRbRBfkVXenAUY2qenq6HF4S0iJ0pjpVDNTANn5aQY7VV55hhsA==", "license": "MIT" }, "node_modules/@expo/sdk-runtime-versions": { @@ -1906,12 +2233,12 @@ "license": "MIT" }, "node_modules/@expo/spawn-async": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", - "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.8.0.tgz", + "integrity": "sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3" + "cross-spawn": "^7.0.6" }, "engines": { "node": ">=12" @@ -1941,9 +2268,9 @@ "license": "MIT" }, "node_modules/@expo/xcpretty": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.1.tgz", - "integrity": "sha512-KZNxZvnGCtiM2aYYZ6Wz0Ix5r47dAvpNLApFtZWnSoERzAdOMzVBOPysBoM0JlF6FKWZ8GPqgn6qt3dV/8Zlpg==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz", + "integrity": "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==", "license": "BSD-3-Clause", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -1961,9 +2288,19 @@ "license": "Python-2.0" }, "node_modules/@expo/xcpretty/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2024,109 +2361,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -2140,7 +2374,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -2157,7 +2390,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2167,190 +2399,67 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", - "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/console/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/console/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/console/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/console/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/console/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/core": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", - "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.2.0", - "jest-config": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-resolve-dependencies": "30.2.0", - "jest-runner": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "jest-watcher": "30.2.0", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0" + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -2361,109 +2470,10 @@ } } }, - "node_modules/@jest/core/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jest/core/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -2476,195 +2486,10 @@ "node": ">=8" } }, - "node_modules/@jest/core/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/core/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/@jest/core/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3" @@ -2687,7 +2512,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", @@ -2700,37 +2524,36 @@ } }, "node_modules/@jest/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.2.0", - "jest-snapshot": "30.2.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0" + "jest-get-type": "^29.6.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -2755,259 +2578,55 @@ } }, "node_modules/@jest/globals": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", - "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/types": "30.2.0", - "jest-mock": "30.2.0" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/globals/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@jest/globals/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/globals/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/globals/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", - "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", + "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", - "string-length": "^4.0.2", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -3018,105 +2637,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jest/reporters/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3125,48 +2645,33 @@ "license": "MIT" }, "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/@jest/reporters/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3189,193 +2694,17 @@ "node": ">=10" } }, - "node_modules/@jest/reporters/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/@jest/reporters/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "*" } }, "node_modules/@jest/schemas": { @@ -3390,293 +2719,57 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/snapshot-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/test-result": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", - "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/types": "30.2.0", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/test-result/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, "node_modules/@jest/test-sequencer": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", - "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/test-sequencer/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/transform": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -3771,43 +2864,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, "node_modules/@react-native-async-storage/async-storage": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", @@ -3821,31 +2877,31 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.85.2.tgz", - "integrity": "sha512-kauC/oPaxklU4Y+u9gBfCBJm51qX6WBZq4xx0USCdimtp+G8+554kpygfSWIjoqCJa2o06bWxBEjesiuCv+LzA==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.83.10.tgz", + "integrity": "sha512-AcVfyQ+8HsWCecXEnSaRffP71KqaWrhNB8bLulYCpKO7i5h/lmwgF191uafU/WiS0LbEMTGlXwWBshPto1phyA==", "license": "MIT", "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.2.tgz", - "integrity": "sha512-XbcN/BEa64pVlb0Hb/E/Ph2SepjVN/FcNKrJcQvtaKZA6mBSO8pW8Eircdlr61/KBH94LihHbQoQDzkQFpeaTg==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.83.10.tgz", + "integrity": "sha512-iRfhxsOePloy12TUzpL9dCwnxLi3eurg8+JurSozAmWh4HtmKu0IJHNjbEKk9htI5n4RlFom8LQ9vVcvgWOTbw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", - "@react-native/codegen": "0.83.2" + "@react-native/codegen": "0.83.10" }, "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/babel-preset": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.2.tgz", - "integrity": "sha512-X/RAXDfe6W+om/Fw1i6htTxQXFhBJ2jgNOWx3WpI3KbjeIWbq7ib6vrpTeIAW2NUMg+K3mML1NzgD4dpZeqdjA==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.83.10.tgz", + "integrity": "sha512-wTZWvs5cUQqr2qBAoJNKUy6IBH7wqPAJdxTC834S/7vBqtXypYHxDAFzYXkIh8pRTC86IBYxyc2IrBYHnB8O2g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -3889,7 +2945,7 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.83.2", + "@react-native/babel-plugin-codegen": "0.83.10", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" @@ -3902,9 +2958,9 @@ } }, "node_modules/@react-native/codegen": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.2.tgz", - "integrity": "sha512-9uK6X1miCXqtL4c759l74N/XbQeneWeQVjoV7SD2CGJuW7ZefxaoYenwGPs7rMoCdtS6wuIyR3hXQ+uWEBGYXA==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.10.tgz", + "integrity": "sha512-rTcuuwRDPLrBmhXc9vAr2gispQFCEqd2WVPh2+N5eV80p8tf9mHy7CoFnmPy8A8csK6HGlANaD9SMPvccQjh4g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -3929,9 +2985,9 @@ "license": "MIT" }, "node_modules/@react-native/codegen/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -3972,25 +3028,25 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.85.2.tgz", - "integrity": "sha512-3KLgSg1kHvBpr93zMaQhvfYTgnCw7yZRED+3J4dMcYjfSjtD0Wf8SofU6uBmAw9JaVYvP43lpdwUpI4p0+ABsg==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.83.10.tgz", + "integrity": "sha512-7QmZ7FLAIJbYjgxILBUE1k71lQB6atXZSLRczKE6Iti+BO3XHNiDSCZxuxUkYUJI0V/0kCbO/M8/a5zqHNu+Rw==", "license": "MIT", "dependencies": { - "@react-native/dev-middleware": "0.85.2", + "@react-native/dev-middleware": "0.83.10", "debug": "^4.4.0", "invariant": "^2.2.4", - "metro": "^0.84.0", - "metro-config": "^0.84.0", - "metro-core": "^0.84.0", + "metro": "^0.83.6", + "metro-config": "^0.83.6", + "metro-core": "^0.83.6", "semver": "^7.1.3" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" }, "peerDependencies": { "@react-native-community/cli": "*", - "@react-native/metro-config": "0.85.2" + "@react-native/metro-config": "*" }, "peerDependenciesMeta": { "@react-native-community/cli": { @@ -4001,421 +3057,19 @@ } } }, - "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.85.2.tgz", - "integrity": "sha512-j+0b9H5f5hGTLQxHIhJU/b/W6ijuxJF+ZTLHB0se2kzUBNxFKd7DkIc6753qk3CJdiv55vxG3XDgmlpbHxOpmA==", - "license": "BSD-3-Clause", - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-shell": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.85.2.tgz", - "integrity": "sha512-r5BkhqPMfg3LmaZS5zadHmBNVH5h4bhSpv4BEPGfK4gat9HABAMzUzybi+2wpgU3SoHxnyKGdExEJvoqVcjeRg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.85.2.tgz", - "integrity": "sha512-3J+NaDUg+QEfDeLAUzgaWhpaxEg78g+KwbydlDCewh2G6WnHpsty8XooruxNHzyAsqVWywZMrzmbn78Ctc1O9Q==", - "license": "MIT", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.85.2", - "@react-native/debugger-shell": "0.85.2", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.3.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/chromium-edge-launcher": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.3.0.tgz", - "integrity": "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^1.0.4" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/hermes-estree": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", - "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", - "license": "MIT" - }, - "node_modules/@react-native/community-cli-plugin/node_modules/hermes-parser": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", - "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.35.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.3.tgz", - "integrity": "sha512-1h3lbVrE6hGf1e/764HfhPGg/bGrWMJDDh7G2rc4gFYZboVuI40BlG/y+UhtbhQDNlO/csMvrcnK0YrTlHUVew==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "accepts": "^2.0.0", - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "error-stack-parser": "^2.0.6", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.35.0", - "image-size": "^1.0.2", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "jsc-safe-url": "^0.2.2", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.84.3", - "metro-cache": "0.84.3", - "metro-cache-key": "0.84.3", - "metro-config": "0.84.3", - "metro-core": "0.84.3", - "metro-file-map": "0.84.3", - "metro-resolver": "0.84.3", - "metro-runtime": "0.84.3", - "metro-source-map": "0.84.3", - "metro-symbolicate": "0.84.3", - "metro-transform-plugins": "0.84.3", - "metro-transform-worker": "0.84.3", - "mime-types": "^3.0.1", - "nullthrows": "^1.1.1", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "throat": "^5.0.0", - "ws": "^7.5.10", - "yargs": "^17.6.2" - }, - "bin": { - "metro": "src/cli.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-babel-transformer": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.3.tgz", - "integrity": "sha512-svAA+yMLpeMiGcz/jKJs4oHpIGEx4nBqNEJ5AGj4CYIg1efvK+A0TjR6tgIuc6tKO5e8JmN/1lglpN2+f3/z/w==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.35.0", - "metro-cache-key": "0.84.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-cache": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.3.tgz", - "integrity": "sha512-0QElxwLaHqLZf+Xqio8QrjVbuXP/8sJfQBGSPiITlKDVXrVLefuzYVSH9Sj+QL6lrPj2gYZd/iwQh1yZuVKnLA==", - "license": "MIT", - "dependencies": { - "exponential-backoff": "^3.1.1", - "flow-enums-runtime": "^0.0.6", - "https-proxy-agent": "^7.0.5", - "metro-core": "0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-cache-key": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.3.tgz", - "integrity": "sha512-TnSL1Fdvrw+2glTdBSRmA5TL8l/i16ECjsrUdf3E5HncA+sNx8KcwDG8r+3ct1UhfYcusJypzZqTN55FZZcwGg==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-config": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.3.tgz", - "integrity": "sha512-JmCzZWOETR+O22q8oPBWyQppx3roU9EbkbGzD8Gf1jukQ4b5T1fTzqqHruu6K4sTiNq5zVQySmKF6bp4kVARew==", - "license": "MIT", - "dependencies": { - "connect": "^3.6.5", - "flow-enums-runtime": "^0.0.6", - "jest-validate": "^29.7.0", - "metro": "0.84.3", - "metro-cache": "0.84.3", - "metro-core": "0.84.3", - "metro-runtime": "0.84.3", - "yaml": "^2.6.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-core": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.3.tgz", - "integrity": "sha512-cc0pvAa80ai1nDmqqz0P59a+0ZqCZ/YHU/3jEekZL6spFnYDfX8iDLdn9FR6kX+67rmzKxHNrbrSRFLX2AYocw==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-file-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.3.tgz", - "integrity": "sha512-1cL4m4Jv1yRUt9RJExZQLfccscdlMNOcRG6LHLtmJhf3BG9j3MujPVc7CIpKYdFl+KUl+sdjge6oO3+meKCHQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "fb-watchman": "^2.0.0", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "nullthrows": "^1.1.1", - "walker": "^1.0.7" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-minify-terser": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.3.tgz", - "integrity": "sha512-3ofrG2OQyJbO9RNhCfOcl8QU7EE2WrSsnN5dFkuZaJO5+4Imujr9bUXmspeNlXRsOVk0F/rVRbEFH98lFSCkBQ==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "terser": "^5.15.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-resolver": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.3.tgz", - "integrity": "sha512-pjEzGDtoM8DTHAIPK/9u9ZxszEiuRohYUVImWvgbnB91V4gqYJpQcoEYUugf2NIm1lrX5HNu0OvNqWmPBnGYjA==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-runtime": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.3.tgz", - "integrity": "sha512-o7HLRfMyVk9N2dUZ9VjQfB6xxUItL9Pi9WcqxURE7MEKOH6wbGt9/E92YdYLluTOtkzYAEVfdC6h6lcxqA+hMQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.0", - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-source-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.3.tgz", - "integrity": "sha512-jS48CeSzw78M8y6VE0f9uy3lVmfbOS677j2VCxnlmlYmnahcXuC6IhoN9K6LynNvos9517yUadcfgioju38xYQ==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-symbolicate": "0.84.3", - "nullthrows": "^1.1.1", - "ob1": "0.84.3", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-symbolicate": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.3.tgz", - "integrity": "sha512-J9Tpo8NCycYrozRvBIUyOwGAu4xkawOsAppmTscFiaegK0WvuDGwIM53GbzVSnytCHjVAF0io5GQxpkrKTuc7g==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-source-map": "0.84.3", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-transform-plugins": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.3.tgz", - "integrity": "sha512-8S3baq2XhBaafHEH5Q8sJW6tmzsEJk80qKc3RU/nZV1MsnYq94RdjTUR6AyKjQd6Rfsk1BtBxhtiNnk7mgslCg==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/metro-transform-worker": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.3.tgz", - "integrity": "sha512-Wjba7PyYktNRsHbPmkx2J2UX32rAzcDXjCu49zPHeF/viJlYJhwRaNePQcHaCRqQ+kmgQT4ThprsnJfDj71ZMA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "metro": "0.84.3", - "metro-babel-transformer": "0.84.3", - "metro-cache": "0.84.3", - "metro-cache-key": "0.84.3", - "metro-minify-terser": "0.84.3", - "metro-source-map": "0.84.3", - "metro-transform-plugins": "0.84.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/ob1": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.3.tgz", - "integrity": "sha512-J7554Ef8bzmKaDY365Afq6PF+qtdnY/d5PKUQFrsKlZHV/N3OGZewVrvDrQDyX5V5NJjTpcAKtlrFZcDr+HvpQ==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, "node_modules/@react-native/debugger-frontend": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.2.tgz", - "integrity": "sha512-t4fYfa7xopbUF5S4+ihNEwgaq4wLZLKLY0Ms8z72lkMteVd3bOX2Foxa8E2wTfRvdhPOkSpOsTeNDmD8ON4DoQ==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.10.tgz", + "integrity": "sha512-AlSOMdXoaSXi4O82f3APvw14e+EfrEvwPerfH2AI3JUrD/yQjFtbIxeyRPd89/MlKIbZU4cwyVFqj96bj39J5g==", "license": "BSD-3-Clause", "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/debugger-shell": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.2.tgz", - "integrity": "sha512-z9go6NJMsLSDJT5MW6VGugRsZHjYvUTwxtsVc3uLt4U9W6T3J6FWI2wHpXIzd2dUkXRfAiRQ3Zi8ZQQ8fRFg9A==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.10.tgz", + "integrity": "sha512-hk9xFI9H412XSX1lpVuKrnxUQe3zIPKxFceYPUwx2L5aZQQ/yjypWkLs+LLldV6eh93B2sBnZbns1oFSE3LQNw==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6", @@ -4426,14 +3080,14 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.2.tgz", - "integrity": "sha512-Zi4EVaAm28+icD19NN07Gh8Pqg/84QQu+jn4patfWKNkcToRFP5vPEbbp0eLOGWS+BVB1d1Fn5lvMrJsBbFcOg==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.10.tgz", + "integrity": "sha512-V39TcESd9LGzDBs7PYVsx5oE1oGv1dLC0GIyJyEyehZjmOYk5sJ4C0m1ATKPeDE7JhiY8s/p6pNOBNp+NF4FGQ==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.83.2", - "@react-native/debugger-shell": "0.83.2", + "@react-native/debugger-frontend": "0.83.10", + "@react-native/debugger-shell": "0.83.10", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", @@ -4449,27 +3103,27 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.85.2.tgz", - "integrity": "sha512-YXBOLeAqFrv7XwUeBPTKZeOV1FIxn4AW7UAEitScf3ibC8bu8+6NpJu4HWgbNQHg7vDbbTZVbcOl8EwGxsSq2w==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.83.10.tgz", + "integrity": "sha512-DpWzrcmxAEgD/tvy4r4WH10PYJPdfDCvtctVZ2Ez0sHlNYgWI6STfqxeAcZhU84hMhNsTec3IHDxNJ2cG8xPjQ==", "license": "MIT", "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, "node_modules/@react-native/js-polyfills": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.2.tgz", - "integrity": "sha512-esGEAmKVM40DV/yVmNljCKZTIeUo7qXqc+Hwffkv3TG+b3E24xyFovHrbP98gGxZr2ZsEyx+2sKLdXF5asY5nw==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.83.10.tgz", + "integrity": "sha512-q28LKHga5lf2ZX4u1T8Bj5RXqyzOBRWSXVjCCdczj/oacAVZPUrandGC1E9fR35fujyb3ZCqZbIQCvs8MhsTNA==", "license": "MIT", "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" } }, "node_modules/@react-native/normalize-colors": { - "version": "0.83.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.2.tgz", - "integrity": "sha512-gkZAb9LoVVzNuYzzOviH7DiPTXQoZPHuiTH2+O2+VWNtOkiznjgvqpwYAhg58a5zfRq5GXlbBdf5mzRj5+3Y5Q==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.10.tgz", + "integrity": "sha512-dWgqcxaBy27oLx9tndcTF0917vbLOOstyNjYD58J6Z7YxkEZSaKG6+A+3I1KV6O1Xg2D69QThp4t6ezoC3NiGA==", "license": "MIT" }, "node_modules/@react-navigation/bottom-tabs": { @@ -4565,14 +3219,17 @@ } }, "node_modules/@shopify/react-native-skia": { - "version": "2.4.21", - "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-2.4.21.tgz", - "integrity": "sha512-US1dIpHbnU63k1S1oOAJ1x4oyuQJ6XDHIAs1kXiDSA4g2jWdH7UPoUHvEllqip2MI2Oc2ZNLsip5Ftegz714oA==", - "hasInstallScript": true, + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@shopify/react-native-skia/-/react-native-skia-2.11.0.tgz", + "integrity": "sha512-XZJoJMhCtdxpe/Ke7Bx3RqXZtdR9DUdECYkQEFFYrCmosTc6VsjSNZYXJdS1MScU7QNbci6t09L3y02jUoMLvw==", "license": "MIT", "peer": true, "dependencies": { - "canvaskit-wasm": "0.40.0", + "canvaskit-wasm": "0.41.0", + "react-native-skia-android": "152.0.0", + "react-native-skia-apple-ios": "152.0.0", + "react-native-skia-apple-macos": "152.0.0", + "react-native-skia-apple-tvos": "152.0.0", "react-reconciler": "0.31.0" }, "bin": { @@ -4581,7 +3238,8 @@ "peerDependencies": { "react": ">=19.0", "react-native": ">=0.78", - "react-native-reanimated": ">=3.19.1" + "react-native-reanimated": ">=4.0.0", + "react-native-worklets": ">=0.7.0" }, "peerDependenciesMeta": { "react-native": { @@ -4589,6 +3247,9 @@ }, "react-native-reanimated": { "optional": true + }, + "react-native-worklets": { + "optional": true } } }, @@ -4602,7 +3263,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -4612,7 +3272,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -4746,9 +3405,9 @@ } }, "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", "dev": true, "license": "MIT", "engines": { @@ -4761,22 +3420,10 @@ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -4790,7 +3437,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -4800,7 +3446,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -4811,7 +3456,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -4835,7 +3479,6 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4872,62 +3515,14 @@ } }, "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "expect": "^29.0.0", + "pretty-format": "^29.0.0" } }, "node_modules/@types/jsdom": { @@ -4980,7 +3575,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, "license": "MIT" }, "node_modules/@types/stats.js": { @@ -5281,275 +3875,6 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@webgpu/types": { "version": "0.1.21", "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz", @@ -5560,6 +3885,7 @@ "version": "0.8.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "deprecated": "this version has critical issues, please update to the latest version", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5653,6 +3979,18 @@ "node": ">= 14" } }, + "node_modules/agent-cli-detector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.6.tgz", + "integrity": "sha512-vKrPeEVN3upDF3GjWxsWBbwQgMtNJ8VB1cduPvK3svmmz4ENpS7yaPxbogsbe3w+xp9xp2Cu+0Ar41rjAR4+lA==", + "license": "MIT", + "bin": { + "agent-cli-detector": "dist/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -5731,7 +4069,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -5751,7 +4088,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -5770,21 +4106,46 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", - "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -5806,7 +4167,6 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -5823,7 +4183,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -5836,13 +4195,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", - "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { @@ -5872,12 +4231,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", - "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5920,7 +4279,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -5944,9 +4302,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "55.0.10", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.10.tgz", - "integrity": "sha512-aRtW7qJKohGU2V0LUJ6IeP7py3+kVUo9zcc8+v1Kix8jGGuIvqvpo9S6W1Fmn9VFP2DBwkFDLiyzkCZS85urVA==", + "version": "55.0.24", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-55.0.24.tgz", + "integrity": "sha512-dzjg70cq3Ls5msL79GSktjqtbjzXh/r5errxz8aD97S180pkXrMga22ozrcNhHddMBtZGDKN2jBK1FS1RLkfBQ==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -5965,7 +4323,7 @@ "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-preset": "0.83.2", + "@react-native/babel-preset": "0.83.10", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", @@ -5976,7 +4334,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^55.0.2", + "expo-widgets": "^55.0.20", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -5995,7 +4353,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -6038,9 +4395,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.11.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz", + "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -6103,9 +4460,9 @@ } }, "node_modules/bplist-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", - "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", "license": "MIT", "dependencies": { "big-integer": "1.6.x" @@ -6115,15 +4472,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -6139,9 +4496,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "funding": [ { "type": "opencollective", @@ -6158,11 +4515,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -6231,9 +4588,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001775", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", - "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "funding": [ { "type": "opencollective", @@ -6251,9 +4608,9 @@ "license": "CC-BY-4.0" }, "node_modules/canvaskit-wasm": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.40.0.tgz", - "integrity": "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.41.0.tgz", + "integrity": "sha512-cnbL02NFB3yOYMF/MtxViZHgD1vh55Pvy+zR8q4JuFvyCPejZP3eClkt2GuZ0S7jOmGMCJXaHBasbMChbR9JZg==", "license": "BSD-3-Clause", "peer": true, "dependencies": { @@ -6325,9 +4682,9 @@ "license": "MIT" }, "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, "license": "MIT" }, @@ -6548,6 +4905,19 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6555,18 +4925,43 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.1" + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -7027,9 +5422,9 @@ } }, "node_modules/dnssd-advertise": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.3.tgz", - "integrity": "sha512-XENsHi3MBzWOCAXif3yZvU1Ah0l+nhJj1sjWL6TnOAYKvGiFhbTx32xHN7+wLMLUOCj7Nr0evADWG4R8JtqCDA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz", + "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, "node_modules/dom-serializer": { @@ -7115,13 +5510,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7129,9 +5517,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", "license": "ISC" }, "node_modules/emittery": { @@ -7361,6 +5749,43 @@ } } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react-hooks/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -7506,7 +5931,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -7630,232 +6054,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/expect/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/expect/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/expect/node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/expect/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/expo": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/expo/-/expo-55.0.4.tgz", - "integrity": "sha512-cbQBPYwmH6FRvh942KR8mSdEcrVdsIMkjdHthtf59zlpzgrk28FabhOdL/Pc9WuS+CsIP3EIQbZqmLkTjv6qPg==", + "version": "55.0.29", + "resolved": "https://registry.npmjs.org/expo/-/expo-55.0.29.tgz", + "integrity": "sha512-mCP2YH+a3lN6QWjaW6cBY7YLXvzCoeiVD76d4LJNrNFgvmN9smM2Qmg6+VTwaGhkt+upYx+GsChA+R7i9QyiPw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "55.0.14", - "@expo/config": "~55.0.8", - "@expo/config-plugins": "~55.0.6", - "@expo/devtools": "55.0.2", - "@expo/fingerprint": "0.16.5", - "@expo/local-build-cache-provider": "55.0.6", - "@expo/log-box": "55.0.7", - "@expo/metro": "~54.2.0", - "@expo/metro-config": "55.0.9", + "@expo/cli": "55.0.35", + "@expo/config": "~55.0.20", + "@expo/config-plugins": "~55.0.11", + "@expo/devtools": "55.0.3", + "@expo/fingerprint": "0.16.8", + "@expo/local-build-cache-provider": "55.0.15", + "@expo/log-box": "55.0.13", + "@expo/metro": "~55.1.1", + "@expo/metro-config": "55.0.26", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~55.0.10", - "expo-asset": "~55.0.8", - "expo-constants": "~55.0.7", - "expo-file-system": "~55.0.10", - "expo-font": "~55.0.4", - "expo-keep-awake": "~55.0.4", - "expo-modules-autolinking": "55.0.8", - "expo-modules-core": "55.0.13", + "babel-preset-expo": "~55.0.24", + "expo-asset": "~55.0.19", + "expo-constants": "~55.0.17", + "expo-file-system": "~55.0.25", + "expo-font": "~55.0.8", + "expo-keep-awake": "~55.0.8", + "expo-modules-autolinking": "55.0.26", + "expo-modules-core": "55.0.25", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", - "whatwg-url-minimum": "^0.1.1" + "whatwg-url-minimum": "^0.1.2" }, "bin": { "expo": "bin/cli", @@ -7881,10 +6134,48 @@ } } }, + "node_modules/expo-asset": { + "version": "55.0.19", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-55.0.19.tgz", + "integrity": "sha512-lhUpS5o7gKfjSBsbPLDYX5+MM65pWZOZ77SZGslhhXlKC5pH/S++Nsk52FfBTiAgdjSdX6SMewYQZnZQ1+n1ZA==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.8.16", + "expo-constants": "~55.0.17" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-constants": { + "version": "55.0.17", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.17.tgz", + "integrity": "sha512-t0SNWmXTjXPCHzjNsv1Okjaj8SpXQcUjDPtYDqvofDS+BhAsvlKNmwaaWXvduBjjmmmJWNH8q1/x9IWQ+upg8g==", + "license": "MIT", + "dependencies": { + "@expo/env": "~2.1.3" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-file-system": { + "version": "55.0.25", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.25.tgz", + "integrity": "sha512-7OSeDRtL34tNqNmcxSf1fXIB0fg+l41uxWERFVvxX3HTt4sp4rdtmFDLc9DS7gBC8lF3m6hDmOPohYX6c+bURg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-font": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.4.tgz", - "integrity": "sha512-ZKeGTFffPygvY5dM/9ATM2p7QDkhsaHopH7wFAWgP2lKzqUMS9B/RxCvw5CaObr9Ro7x9YptyeRKX2HmgmMfrg==", + "version": "55.0.8", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-55.0.8.tgz", + "integrity": "sha512-WyP75pnKqhLNktYwDn3xKAUNt5rLihRDv8XWGhhz6VEhVqypixpT86NA3uGtiDTlM3gGjhrYCY7o7ypXgCUOZg==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -7895,13 +6186,23 @@ "react-native": "*" } }, - "node_modules/expo-modules-autolinking": { + "node_modules/expo-keep-awake": { "version": "55.0.8", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-55.0.8.tgz", - "integrity": "sha512-nrWB1pkNp7bR8ECUTgYUiJ2Pyh6AvxCBXZ+lyPlfl1TzEIGhwU1Yqr+d78eJDueXaW+9zKeE0HqrTZoLS3ve4A==", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.8.tgz", + "integrity": "sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-modules-autolinking": { + "version": "55.0.26", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-55.0.26.tgz", + "integrity": "sha512-eHea+mDQJbKBYTRNUouRwN0GLxdWl/erG/20VL0awcpsJsBB2z0LbOTJzYKUDKw0XK5Sc3kXXKHoVoCR3+ARZQ==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^55.0.2", + "@expo/require-utils": "^55.0.7", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" @@ -7911,31 +6212,37 @@ } }, "node_modules/expo-modules-core": { - "version": "55.0.13", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-55.0.13.tgz", - "integrity": "sha512-DYLQTOJAR7jD3M9S0sH9myZaPEtShdicHrPiWcupIXMeMkQxFzErx+adUI8gZPy4AU45BgeGgtaogRfT25iLfw==", + "version": "55.0.25", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-55.0.25.tgz", + "integrity": "sha512-yXpfg7aHLbuqoXocK34Vua6Aey5SCyqLygAsXAMbul9P8vfBjLpaOPiTJ5cLVF7Drfq8ownqVJO6qpGEtZ6GOw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-worklets": "^0.7.4 || ^0.8.0" + }, + "peerDependenciesMeta": { + "react-native-worklets": { + "optional": true + } } }, "node_modules/expo-server": { - "version": "55.0.6", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.6.tgz", - "integrity": "sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ==", + "version": "55.0.12", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.12.tgz", + "integrity": "sha512-yuNHbxobUKCXWn9Z/H+lgYk6heXEFub6q1h8Yo9kCLxgL2afluWZ/YylbtxgHgmFwDVUmi6milQYfL+wwQMlzw==", "license": "MIT", "engines": { "node": ">=20.16.0" } }, "node_modules/expo-status-bar": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.4.tgz", - "integrity": "sha512-BPDjUXKqv1F9j2YNGLRZfkBEZXIEEpqj+t81y4c+4fdSN3Pos7goIHXgcl2ozbKQLgKRZQyNZQtbUgh5UjHYUQ==", + "version": "55.0.6", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.6.tgz", + "integrity": "sha512-ijOUptfdiqYt7rObZ6jrPQ8sE5YN/8MxKCIJx0b7TY4nGkSJxhPIxeoW4GXcXCA8mTQ9PiOHH/ThLZgRVZvUlQ==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" @@ -7945,290 +6252,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/@expo/cli": { - "version": "55.0.14", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.14.tgz", - "integrity": "sha512-glXPSjjLCIz+KX/ezqLTGIF9eTE1lexiCxunvB3loRZNnGeBDGW3eF++cuPKudW26jeC6bqZkcqBG7Lp0Sp9qg==", - "license": "MIT", - "dependencies": { - "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~55.0.8", - "@expo/config-plugins": "~55.0.6", - "@expo/devcert": "^1.2.1", - "@expo/env": "~2.1.1", - "@expo/image-utils": "^0.8.12", - "@expo/json-file": "^10.0.12", - "@expo/log-box": "55.0.7", - "@expo/metro": "~54.2.0", - "@expo/metro-config": "~55.0.9", - "@expo/osascript": "^2.4.2", - "@expo/package-manager": "^1.10.3", - "@expo/plist": "^0.5.2", - "@expo/prebuild-config": "^55.0.8", - "@expo/require-utils": "^55.0.2", - "@expo/router-server": "^55.0.9", - "@expo/schema-utils": "^55.0.2", - "@expo/spawn-async": "^1.7.2", - "@expo/ws-tunnel": "^1.0.1", - "@expo/xcpretty": "^4.4.0", - "@react-native/dev-middleware": "0.83.2", - "accepts": "^1.3.8", - "arg": "^5.0.2", - "better-opn": "~3.0.2", - "bplist-creator": "0.1.0", - "bplist-parser": "^0.3.1", - "chalk": "^4.0.0", - "ci-info": "^3.3.0", - "compression": "^1.7.4", - "connect": "^3.7.0", - "debug": "^4.3.4", - "dnssd-advertise": "^1.1.3", - "expo-server": "^55.0.6", - "fetch-nodeshim": "^0.4.6", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "lan-network": "^0.2.0", - "multitars": "^0.2.3", - "node-forge": "^1.3.3", - "npm-package-arg": "^11.0.0", - "ora": "^3.4.0", - "picomatch": "^4.0.3", - "pretty-format": "^29.7.0", - "progress": "^2.0.3", - "prompts": "^2.3.2", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "send": "^0.19.0", - "slugify": "^1.3.4", - "source-map-support": "~0.5.21", - "stacktrace-parser": "^0.1.10", - "structured-headers": "^0.4.1", - "terminal-link": "^2.1.1", - "toqr": "^0.1.1", - "wrap-ansi": "^7.0.0", - "ws": "^8.12.1", - "zod": "^3.25.76" - }, - "bin": { - "expo-internal": "build/bin/cli" - }, - "peerDependencies": { - "expo": "*", - "expo-router": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "expo-router": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": { - "version": "55.0.8", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.8.tgz", - "integrity": "sha512-VJNJiOmmZgyDnR7JMmc3B8Z0ZepZ17I8Wtw+wAH/2+UCUsFg588XU+bwgYcFGw+is28kwGjY46z43kfufpxOnA==", - "license": "MIT", - "dependencies": { - "@expo/config": "~55.0.8", - "@expo/config-plugins": "~55.0.6", - "@expo/config-types": "^55.0.5", - "@expo/image-utils": "^0.8.12", - "@expo/json-file": "^10.0.12", - "@react-native/normalize-colors": "0.83.2", - "debug": "^4.3.1", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "xml2js": "0.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "55.0.9", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.9.tgz", - "integrity": "sha512-LcCFi+P1qfZOsw0DO4JwNKRxtWt4u2bjTYj0PUe4WVf9NVG/NfUetAXYRbBS6P+gupfM6SC+/bdzdqCWQh7j8g==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "@expo/metro-runtime": "^55.0.6", - "expo": "*", - "expo-constants": "^55.0.7", - "expo-font": "^55.0.4", - "expo-router": "*", - "expo-server": "^55.0.6", - "react": "*", - "react-dom": "*", - "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" - }, - "peerDependenciesMeta": { - "@expo/metro-runtime": { - "optional": true - }, - "expo-router": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-server-dom-webpack": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/log-box": { - "version": "55.0.7", - "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.7.tgz", - "integrity": "sha512-m7V1k2vlMp4NOj3fopjOg4zl/ANXyTRF3HMTMep2GZAKsPiDzgOQ41nm8CaU50/HlDIGXlCObss07gOn20UpHQ==", - "license": "MIT", - "dependencies": { - "@expo/dom-webview": "^55.0.3", - "anser": "^1.4.9", - "stacktrace-parser": "^0.1.10" - }, - "peerDependencies": { - "@expo/dom-webview": "^55.0.3", - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/@expo/log-box/node_modules/@expo/dom-webview": { - "version": "55.0.3", - "resolved": "https://registry.npmjs.org/@expo/dom-webview/-/dom-webview-55.0.3.tgz", - "integrity": "sha512-bY4/rfcZ0f43DvOtMn8/kmPlmo01tex5hRoc5hKbwBwQjqWQuQt0ACwu7akR9IHI4j0WNG48eL6cZB6dZUFrzg==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "55.0.9", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.9.tgz", - "integrity": "sha512-ZJFEfat/+dLUhFyFFWrzMjAqAwwUaJ3RD42QNqR7jh+RVYkAf6XYLynb5qrKJTHI1EcOx4KoO1717yXYYRFDBA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~55.0.8", - "@expo/env": "~2.1.1", - "@expo/json-file": "~10.0.12", - "@expo/metro": "~54.2.0", - "@expo/spawn-async": "^1.7.2", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.32.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.3", - "postcss": "~8.4.32", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/expo/node_modules/expo-asset": { - "version": "55.0.8", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-55.0.8.tgz", - "integrity": "sha512-yEz2svDX67R0yiW2skx6dJmcE0q7sj9ECpGMcxBExMCbctc+nMoZCnjUuhzPl5vhClUsO5HFFXS5vIGmf1bgHQ==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.8.12", - "expo-constants": "~55.0.7" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-constants": { - "version": "55.0.7", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.7.tgz", - "integrity": "sha512-kdcO4TsQRRqt0USvjaY5vgQMO9H52K3kBZ/ejC7F6rz70mv08GoowrZ1CYOr5O4JpPDRlIpQfZJUucaS/c+KWQ==", - "license": "MIT", - "dependencies": { - "@expo/config": "~55.0.8", - "@expo/env": "~2.1.1" - }, - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-file-system": { - "version": "55.0.10", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz", - "integrity": "sha512-ysFdVdUgtfj2ApY0Cn+pBg+yK4xp+SNwcaH8j2B91JJQ4OXJmnyCSmrNZYz7J4mdYVuv2GzxIP+N/IGlHQG3Yw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-keep-awake": { - "version": "55.0.4", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz", - "integrity": "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, - "node_modules/expo/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -8245,7 +6268,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -8310,6 +6332,7 @@ "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" @@ -8324,9 +6347,9 @@ } }, "node_modules/fetch-nodeshim": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.8.tgz", - "integrity": "sha512-YW5vG33rabBq6JpYosLNoXoaMN69/WH26MeeX2hkDVjN6UlvRGq3Wkazl9H0kisH95aMu/HtHL64JUvv/+Nv/g==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/fetch-nodeshim/-/fetch-nodeshim-0.4.10.tgz", + "integrity": "sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==", "license": "MIT" }, "node_modules/fflate": { @@ -8406,7 +6429,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -8431,9 +6453,9 @@ } }, "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -8444,9 +6466,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8469,47 +6491,17 @@ "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", "license": "BSD-2-Clause" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -8534,7 +6526,6 @@ "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, @@ -8600,7 +6591,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -8671,6 +6661,19 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8726,9 +6729,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8738,9 +6741,9 @@ } }, "node_modules/hermes-compiler": { - "version": "250829098.0.10", - "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.10.tgz", - "integrity": "sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-0.14.1.tgz", + "integrity": "sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA==", "license": "MIT" }, "node_modules/hermes-estree": { @@ -8919,21 +6922,6 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -8958,7 +6946,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -9025,12 +7012,12 @@ "license": "MIT" }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -9156,7 +7143,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -9166,7 +7152,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -9183,7 +7168,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9205,20 +7189,30 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, "engines": { "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -9245,39 +7239,23 @@ "react": ">=18.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", - "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/types": "30.2.0", - "import-local": "^3.2.0", - "jest-cli": "30.2.0" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -9289,91 +7267,18 @@ } }, "node_modules/jest-changed-files": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", - "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.2.0", + "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-changed-files/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-changed-files/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-changed-files/node_modules/p-limit": { @@ -9393,233 +7298,35 @@ } }, "node_modules/jest-circus": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", - "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "chalk": "^4.1.2", + "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "30.2.0", - "pure-rand": "^7.0.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "stack-utils": "^2.0.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-circus/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-circus/node_modules/p-limit": { @@ -9638,44 +7345,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/jest-cli": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", - "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "yargs": "^17.7.2" + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -9686,372 +7379,52 @@ } } }, - "node_modules/jest-cli/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli/node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/jest-config": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.2.0", - "@jest/types": "30.2.0", - "babel-jest": "30.2.0", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.2.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-runner": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "micromatch": "^4.0.8", + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "30.2.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@types/node": "*", - "esbuild-register": ">=3.4.0", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "esbuild-register": { - "optional": true - }, "ts-node": { "optional": true } } }, - "node_modules/jest-config/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-config/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/babel-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", - "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.2.0", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.2.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" - } - }, - "node_modules/jest-config/node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", - "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/babel-preset-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", - "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.2.0", - "babel-preset-current-node-syntax": "^1.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" - } - }, "node_modules/jest-config/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10060,19 +7433,20 @@ "license": "MIT" }, "node_modules/jest-config/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/jest-config/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -10086,283 +7460,38 @@ } }, "node_modules/jest-config/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-config/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-config/node_modules/jest-environment-node": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-config/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/jest-config/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-config/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "*" } }, "node_modules/jest-diff": { @@ -10382,134 +7511,33 @@ } }, "node_modules/jest-docblock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", - "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "license": "MIT", "dependencies": { - "detect-newline": "^3.1.0" + "detect-newline": "^3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-each": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", - "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "chalk": "^4.1.2", - "jest-util": "30.2.0", - "pretty-format": "30.2.0" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-each/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-jsdom": { @@ -10540,15 +7568,32 @@ } } }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-expo": { - "version": "55.0.9", - "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-55.0.9.tgz", - "integrity": "sha512-6wz7JJUeW2e0+APRQP7eOcXKPdI7bdmAIoBiPJbtzSBRlghho8LzPcv4jkoVFoYi8SKb9k3BTKx4GcUlyVMedw==", + "version": "55.0.21", + "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-55.0.21.tgz", + "integrity": "sha512-r+tFZjxH/b96wnOunFMZLo9usRDu1yScFeQF6THTjNMzFJsd0uQcIj0YDL1wQb0Ta2I+zu04tZ6FtI5wIehGzw==", "dev": true, "license": "MIT", "dependencies": { - "@expo/config": "~55.0.8", - "@expo/json-file": "^10.0.12", + "@expo/config": "~55.0.20", + "@expo/json-file": "^10.0.15", "@jest/create-cache-key-function": "^29.2.1", "@jest/globals": "^29.2.1", "babel-jest": "^29.2.1", @@ -10576,98 +7621,6 @@ } } }, - "node_modules/jest-expo/node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-expo/node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-expo/node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-expo/node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-expo/node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -10681,7 +7634,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -10704,65 +7656,17 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", - "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.2.0" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-leak-detector/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { @@ -10785,7 +7689,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", @@ -10806,7 +7709,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -10839,594 +7741,77 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", - "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", - "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.2.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-resolve/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", - "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/environment": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "chalk": "^4.1.2", + "chalk": "^4.0.0", "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-leak-detector": "30.2.0", - "jest-message-util": "30.2.0", - "jest-resolve": "30.2.0", - "jest-runtime": "30.2.0", - "jest-util": "30.2.0", - "jest-watcher": "30.2.0", - "jest-worker": "30.2.0", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runner/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-runner/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-runner/node_modules/jest-environment-node": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-runner/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner/node_modules/p-limit": { @@ -11445,34 +7830,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-runner/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-runner/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11494,211 +7851,38 @@ "source-map": "^0.6.0" } }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/jest-runtime": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/globals": "30.2.0", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-mock": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runtime/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runtime/node_modules/balanced-match": { @@ -11709,618 +7893,81 @@ "license": "MIT" }, "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/jest-runtime/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-runtime/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-runtime/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-runtime/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/jest-runtime/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "*" } }, "node_modules/jest-snapshot": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.2.0", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.2.0", - "graceful-fs": "^4.2.11", - "jest-diff": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "pretty-format": "30.2.0", - "semver": "^7.7.2", - "synckit": "^0.11.8" + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-snapshot/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util": { @@ -12420,50 +8067,6 @@ "jest": "^27.0.0 || ^28.0.0 || ^29.0.0" } }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", @@ -12490,79 +8093,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-watch-typeahead/node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -12619,110 +8149,24 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/jest-watch-typeahead/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-watcher": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", - "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "30.2.0", - "string-length": "^4.0.2" + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-watcher/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -12755,45 +8199,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jest/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, "node_modules/jimp-compact": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", @@ -12807,10 +8212,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -12900,9 +8304,9 @@ } }, "node_modules/jsdom/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { @@ -12993,9 +8397,9 @@ } }, "node_modules/lan-network": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.2.0.tgz", - "integrity": "sha512-EZgbsXMrGS+oK+Ta12mCjzBFse+SIewGdwrSTr5g+MSymnjpox2x05ceI20PQejJOFvOgzcXrfDk/SdY7dSCtw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.2.1.tgz", + "integrity": "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==", "license": "MIT", "bin": { "lan-network": "dist/lan-network-cli.js" @@ -13050,9 +8454,9 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -13065,23 +8469,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "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.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -13099,9 +8503,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "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" ], @@ -13119,9 +8523,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "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" ], @@ -13139,9 +8543,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "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" ], @@ -13159,9 +8563,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "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" ], @@ -13179,9 +8583,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "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" ], @@ -13199,9 +8603,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "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" ], @@ -13219,9 +8623,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "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" ], @@ -13239,9 +8643,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "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" ], @@ -13259,9 +8663,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "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" ], @@ -13279,9 +8683,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "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" ], @@ -13309,7 +8713,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -13319,9 +8722,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -13518,45 +8921,43 @@ "license": "MIT" }, "node_modules/metro": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", - "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.8.tgz", + "integrity": "sha512-ZbJDJCDlvv0O3QHVbhZkyP2/DtebfEoDZ3wVyYHgu6Uoy6DmmE/bis6lIDBYh+kWPuaueJDegyutPjkIBLtZDQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", + "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.3", - "@babel/types": "^7.25.2", - "accepts": "^1.3.7", - "chalk": "^4.0.0", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", - "hermes-parser": "0.32.0", - "image-size": "^1.0.2", + "hermes-parser": "0.35.0", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.83.3", - "metro-cache": "0.83.3", - "metro-cache-key": "0.83.3", - "metro-config": "0.83.3", - "metro-core": "0.83.3", - "metro-file-map": "0.83.3", - "metro-resolver": "0.83.3", - "metro-runtime": "0.83.3", - "metro-source-map": "0.83.3", - "metro-symbolicate": "0.83.3", - "metro-transform-plugins": "0.83.3", - "metro-transform-worker": "0.83.3", - "mime-types": "^2.1.27", + "metro-babel-transformer": "0.83.8", + "metro-cache": "0.83.8", + "metro-cache-key": "0.83.8", + "metro-config": "0.83.8", + "metro-core": "0.83.8", + "metro-file-map": "0.83.8", + "metro-resolver": "0.83.8", + "metro-runtime": "0.83.8", + "metro-source-map": "0.83.8", + "metro-symbolicate": "0.83.8", + "metro-transform-plugins": "0.83.8", + "metro-transform-worker": "0.83.8", + "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", @@ -13572,39 +8973,55 @@ } }, "node_modules/metro-babel-transformer": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", - "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.8.tgz", + "integrity": "sha512-tnn0J5wzgTgTx2OJy3Cwr1y79bJz4eNgFQd+2HENOs5Vz6QOMnt05z7J+BedIo9wIbpEa0iN9U1nerxyvMRE9g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.32.0", + "hermes-parser": "0.35.0", + "metro-cache-key": "0.83.8", "nullthrows": "^1.1.1" }, "engines": { "node": ">=20.19.4" } }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", + "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", + "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.35.0" + } + }, "node_modules/metro-cache": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", - "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.8.tgz", + "integrity": "sha512-aogMG5WbKzW5000otNjYrS9hIoORzkCI1faPJK+vxQLaf2BorJKBBFe/jl2Tfsi1mZUglS2EBuBt8B3JB7MYDQ==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", - "metro-core": "0.83.3" + "metro-core": "0.83.8" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-cache-key": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", - "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.8.tgz", + "integrity": "sha512-I38PtcjT4crS5HY9UQ8i6z8S7tJ2WewtPGr/OwS6FcLKfy5T/1hlTaFw+wozUZkEtNpR6Gc0oJuvuKCbSoSN5A==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -13614,18 +9031,18 @@ } }, "node_modules/metro-config": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", - "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.8.tgz", + "integrity": "sha512-crNbNy+/B4tCne2+HjUshwvC57gNBQj+V9fFSy3lHH2RlKcLHib3Mil/SfTX8Pfh2fCY5pPuSu2OKa7YiadB+Q==", "license": "MIT", "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", - "metro": "0.83.3", - "metro-cache": "0.83.3", - "metro-core": "0.83.3", - "metro-runtime": "0.83.3", + "metro": "0.83.8", + "metro-cache": "0.83.8", + "metro-core": "0.83.8", + "metro-runtime": "0.83.8", "yaml": "^2.6.1" }, "engines": { @@ -13633,23 +9050,23 @@ } }, "node_modules/metro-core": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", - "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.8.tgz", + "integrity": "sha512-NTyOUOQaQKvQgJG9VI2ymN6KTM7gHEqkVFhkPc9bK4BsHSTz/EaXuFBXtC+wtwINLeH9tbusL/jfsSmdEmuU7A==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", - "metro-resolver": "0.83.3" + "metro-resolver": "0.83.8" }, "engines": { "node": ">=20.19.4" } }, "node_modules/metro-file-map": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", - "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.8.tgz", + "integrity": "sha512-+W++EUuzEXIfWQEFTWQMVThzhWbnJL4gRNJ9WSHzIAM5pT7gsprUxo9+2hrfirURm/TLvrKwhq37oCECJcDSyQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -13667,9 +9084,9 @@ } }, "node_modules/metro-minify-terser": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", - "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.8.tgz", + "integrity": "sha512-7tU0J5/c7LZaZJwTlOb1xq0NepTFvGzRxigDhZOq4jSE6g0BRHmBqe7XvLH3WcRiJNGy+eshcZr34RpMjb6mmg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -13680,9 +9097,9 @@ } }, "node_modules/metro-resolver": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", - "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.8.tgz", + "integrity": "sha512-piU0NVTI9i37YztDVF5rtn9uxP3NebVT0xZM9NKJ9z0jbigrctUQksi3NoYIeJMvL6Wn2dgAehpcmmnfn+gUwA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -13692,9 +9109,9 @@ } }, "node_modules/metro-runtime": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", - "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.8.tgz", + "integrity": "sha512-f7FfeM0pamq8vrvs8aO9KvIUabbUKe0WkHFpLt6Q9yIIIsORqNFwlgJeHGraOFPU7Cxqj5yLXkJu5bT1uwDXvw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -13705,19 +9122,18 @@ } }, "node_modules/metro-source-map": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", - "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.8.tgz", + "integrity": "sha512-60Uor7bM+KsVewLkLCcZfkPFCbqjPDdoSmeBuT3+ye+ac80BuLWbbR8DpyxWpUqQeZPdaAT5ZqFgDIs8BNEcVA==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.3", - "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", - "@babel/types": "^7.25.2", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-symbolicate": "0.83.3", + "metro-symbolicate": "0.83.8", "nullthrows": "^1.1.1", - "ob1": "0.83.3", + "ob1": "0.83.8", "source-map": "^0.5.6", "vlq": "^1.0.0" }, @@ -13726,14 +9142,14 @@ } }, "node_modules/metro-symbolicate": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", - "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.8.tgz", + "integrity": "sha512-ZLrbqOqQ+m+Mpv7zo65XEXkNqVYvgUbY7tIsb9e1zSSmU/4C3D6DjaJvRHqcAdl0mkg8TI/csKooAEa5xcZNwg==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", - "metro-source-map": "0.83.3", + "metro-source-map": "0.83.8", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" @@ -13746,15 +9162,15 @@ } }, "node_modules/metro-transform-plugins": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", - "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.8.tgz", + "integrity": "sha512-9JRPkvi+m0QH2Y/w5RjCF9mHqOUNFpMFLDDLhKUjhcKESh8Wm3HKDdHXHVldTN1lTToCIabv81nF0zyJzKEJ5g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.3", + "@babel/generator": "^7.29.1", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" }, @@ -13763,29 +9179,98 @@ } }, "node_modules/metro-transform-worker": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", - "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.8.tgz", + "integrity": "sha512-Pa2hOfhUmWpI/dmkhsLq8uGyFHK2opoEK7j/YCiRtqNSe0YzzxYguXTNgg8AMiuZfc9LPcB1AhGENS10O5wcIw==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/types": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", - "metro": "0.83.3", - "metro-babel-transformer": "0.83.3", - "metro-cache": "0.83.3", - "metro-cache-key": "0.83.3", - "metro-minify-terser": "0.83.3", - "metro-source-map": "0.83.3", - "metro-transform-plugins": "0.83.3", + "metro": "0.83.8", + "metro-babel-transformer": "0.83.8", + "metro-cache": "0.83.8", + "metro-cache-key": "0.83.8", + "metro-minify-terser": "0.83.8", + "metro-source-map": "0.83.8", + "metro-transform-plugins": "0.83.8", "nullthrows": "^1.1.1" }, "engines": { "node": ">=20.19.4" } }, + "node_modules/metro/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", + "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", + "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.35.0" + } + }, + "node_modules/metro/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/metro/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/metro/node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -13894,15 +9379,15 @@ "license": "MIT" }, "node_modules/multitars": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/multitars/-/multitars-0.2.4.tgz", - "integrity": "sha512-XgLbg1HHchFauMCQPRwMj6MSyDd5koPlTA1hM3rUFkeXzGpjU/I9fP3to7yrObE9jcN8ChIOQGrM0tV0kUZaKg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.2.tgz", + "integrity": "sha512-6GwVw5eLi9sThdtlS4PKwC7yRLaf45pYhIEzKBHdKxi+YOXGKFX8acIniH+Uh/+k9mS2lQOupTccjoe5r0/1IQ==", "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -13917,22 +9402,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14007,16 +9476,18 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14076,9 +9547,9 @@ "license": "MIT" }, "node_modules/ob1": { - "version": "0.83.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", - "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", + "version": "0.83.8", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.8.tgz", + "integrity": "sha512-pk7el+eTOzfSKMAY4QBiiwKzegXn633JQj13y+pW5E5IdS+yV2CfDJ9Hf20K/LKy8nCrP9dAmd7XOk52OJxifA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -14285,7 +9756,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -14301,7 +9771,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -14314,19 +9783,11 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -14397,7 +9858,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14444,9 +9904,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -14474,7 +9934,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -14494,12 +9953,12 @@ } }, "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", + "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, @@ -14517,9 +9976,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -14536,7 +9995,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14675,9 +10134,9 @@ } }, "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true, "funding": [ { @@ -14716,15 +10175,6 @@ "dev": true, "license": "MIT" }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14754,15 +10204,15 @@ } }, "node_modules/react-dom": { - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.6" + "react": "^19.2.0" } }, "node_modules/react-fast-compare": { @@ -14790,30 +10240,34 @@ "license": "MIT" }, "node_modules/react-native": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.85.2.tgz", - "integrity": "sha512-GFWEPwLYirfj5X8gMtXOWtqX0cqUEURRHETZfFk37VCa4++izrKvGvv24anvuyulXV87NAhVkfNw93rLg3HByw==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.83.10.tgz", + "integrity": "sha512-4gbmtAyfC0F4jU2hl/l/HZbRIfRl0d5RdHNpJLf7sT/0zhYDEhiFcFk7ii2utl7O8BIHRUmGe/anfWP3DNPTZw==", "license": "MIT", "dependencies": { - "@react-native/assets-registry": "0.85.2", - "@react-native/codegen": "0.85.2", - "@react-native/community-cli-plugin": "0.85.2", - "@react-native/gradle-plugin": "0.85.2", - "@react-native/js-polyfills": "0.85.2", - "@react-native/normalize-colors": "0.85.2", - "@react-native/virtualized-lists": "0.85.2", + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.83.10", + "@react-native/codegen": "0.83.10", + "@react-native/community-cli-plugin": "0.83.10", + "@react-native/gradle-plugin": "0.83.10", + "@react-native/js-polyfills": "0.83.10", + "@react-native/normalize-colors": "0.83.10", + "@react-native/virtualized-lists": "0.83.10", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", - "babel-plugin-syntax-hermes-parser": "0.33.3", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.32.0", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", - "hermes-compiler": "250829098.0.10", + "glob": "^7.1.1", + "hermes-compiler": "0.14.1", "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", - "metro-runtime": "^0.84.0", - "metro-source-map": "^0.84.0", + "metro-runtime": "^0.83.6", + "metro-source-map": "^0.83.6", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", @@ -14823,7 +10277,6 @@ "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", - "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" @@ -14832,17 +10285,13 @@ "react-native": "cli.js" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" }, "peerDependencies": { - "@react-native/jest-preset": "0.85.2", "@types/react": "^19.1.1", - "react": "^19.2.3" + "react": "^19.2.0" }, "peerDependenciesMeta": { - "@react-native/jest-preset": { - "optional": true - }, "@types/react": { "optional": true } @@ -14924,6 +10373,34 @@ "react-native": "*" } }, + "node_modules/react-native-skia-android": { + "version": "152.0.0", + "resolved": "https://registry.npmjs.org/react-native-skia-android/-/react-native-skia-android-152.0.0.tgz", + "integrity": "sha512-X++QG8bpPLHpavUyNfQvAxvtjH3r8fnivxS1PTFZsKL4ZiAJ1V9IVsJi4qltafWJbzhq7IutpZQNYd06uwNkjg==", + "license": "MIT", + "peer": true + }, + "node_modules/react-native-skia-apple-ios": { + "version": "152.0.0", + "resolved": "https://registry.npmjs.org/react-native-skia-apple-ios/-/react-native-skia-apple-ios-152.0.0.tgz", + "integrity": "sha512-rmnHYWkwvxm7B9fXZCkTRdZVoQwVnNydjytfphxatweQmEqwYE+/g1LOL/emhM/ulmwAFKFT0VNAEzAlvHId2A==", + "license": "MIT", + "peer": true + }, + "node_modules/react-native-skia-apple-macos": { + "version": "152.0.0", + "resolved": "https://registry.npmjs.org/react-native-skia-apple-macos/-/react-native-skia-apple-macos-152.0.0.tgz", + "integrity": "sha512-boWCDyPPJUuKwcE+DU8X7dop6EwLdrOH+v7pxYAj+43NbOwgfV1wzq4g+kWIQqag5ZOQInq2mVwCc8S1eJjlXw==", + "license": "MIT", + "peer": true + }, + "node_modules/react-native-skia-apple-tvos": { + "version": "152.0.0", + "resolved": "https://registry.npmjs.org/react-native-skia-apple-tvos/-/react-native-skia-apple-tvos-152.0.0.tgz", + "integrity": "sha512-+UnXke4Yz9Jmy3uTcDbYFrVG/+CZMqYbaTV3f52wovaMzf8wWy+iZampZr6dW6rOWLWrsUFiq7yMImg6UGbiFg==", + "license": "MIT", + "peer": true + }, "node_modules/react-native-svg": { "version": "15.15.3", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz", @@ -15116,49 +10593,22 @@ "node": ">=10" } }, - "node_modules/react-native/node_modules/@react-native/codegen": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.85.2.tgz", - "integrity": "sha512-XCginmxh0//++EXVOEJHBVZxHla294FzLCFF6jXwAUjvXVhqyIKyxhABfz+r4OOmaiuWk4Rtd4arqdAzeHeprg==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "hermes-parser": "0.33.3", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/react-native/node_modules/@react-native/normalize-colors": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.85.2.tgz", - "integrity": "sha512-svuOLtjbFGXDdHsriHXuND5FgHg7XlkOXCbH/8+X4t76YLH6qSTffSIQQrKLDL5mn4EFU+Oh/PNO0/FfpnTOTg==", - "license": "MIT" - }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { - "version": "0.85.2", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.85.2.tgz", - "integrity": "sha512-wmVKpAlcr+UB0L5SpbrV865EdleUP7I5+X+48e1aRsQK8q+wsTRBXeUwWVip/1l+HZwlZFeO8iOILJ16VRu0Cw==", + "version": "0.83.10", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.83.10.tgz", + "integrity": "sha512-KNX6jCYcCnOKfoJyGMcgDrAVaQXOuNymBi2UC9sf2msEmgkrGgoWa+eKiATqaCgN44UFKlJIuNj4wUPTCWzZ0Q==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": ">= 20.19.4" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", - "react-native": "0.85.2" + "react-native": "*" }, "peerDependenciesMeta": { "@types/react": { @@ -15166,13 +10616,20 @@ } } }, - "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.33.3.tgz", - "integrity": "sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==", + "node_modules/react-native/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/react-native/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { - "hermes-parser": "0.33.3" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/react-native/node_modules/commander": { @@ -15184,84 +10641,37 @@ "node": ">=18" } }, - "node_modules/react-native/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT" - }, - "node_modules/react-native/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", + "node_modules/react-native/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/react-native/node_modules/metro-runtime": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.3.tgz", - "integrity": "sha512-o7HLRfMyVk9N2dUZ9VjQfB6xxUItL9Pi9WcqxURE7MEKOH6wbGt9/E92YdYLluTOtkzYAEVfdC6h6lcxqA+hMQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.0", - "flow-enums-runtime": "^0.0.6" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-native/node_modules/metro-source-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.3.tgz", - "integrity": "sha512-jS48CeSzw78M8y6VE0f9uy3lVmfbOS677j2VCxnlmlYmnahcXuC6IhoN9K6LynNvos9517yUadcfgioju38xYQ==", - "license": "MIT", + "node_modules/react-native/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-symbolicate": "0.84.3", - "nullthrows": "^1.1.1", - "ob1": "0.84.3", - "source-map": "^0.5.6", - "vlq": "^1.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/react-native/node_modules/metro-symbolicate": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.3.tgz", - "integrity": "sha512-J9Tpo8NCycYrozRvBIUyOwGAu4xkawOsAppmTscFiaegK0WvuDGwIM53GbzVSnytCHjVAF0io5GQxpkrKTuc7g==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-source-map": "0.84.3", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/react-native/node_modules/ob1": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.3.tgz", - "integrity": "sha512-J7554Ef8bzmKaDY365Afq6PF+qtdnY/d5PKUQFrsKlZHV/N3OGZewVrvDrQDyX5V5NJjTpcAKtlrFZcDr+HvpQ==", - "license": "MIT", - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + "node": "*" } }, "node_modules/react-reconciler": { @@ -15407,11 +10817,12 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -15454,6 +10865,16 @@ "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==", "license": "MIT" }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -15490,9 +10911,9 @@ "license": "MIT" }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -15560,9 +10981,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -15751,9 +11172,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -15779,6 +11200,18 @@ "plist": "^3.0.5" } }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -15798,16 +11231,15 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/slugify": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -15863,7 +11295,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/stack-generator": { @@ -15880,7 +11311,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -15893,7 +11323,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16005,22 +11434,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -16033,20 +11446,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -16149,22 +11548,6 @@ "dev": true, "license": "MIT" }, - "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -16209,7 +11592,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -16224,14 +11606,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -16243,7 +11623,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -16264,7 +11643,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -16289,6 +11667,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -16376,14 +11755,6 @@ "typescript": ">=4.8.4" } }, - "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -16401,7 +11772,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -16521,45 +11891,10 @@ "node": ">= 0.8" } }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "funding": [ { "type": "opencollective", @@ -16638,6 +11973,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -16794,9 +12130,9 @@ } }, "node_modules/whatwg-url-minimum": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.1.tgz", - "integrity": "sha512-u2FNVjFVFZhdjb502KzXy1gKn1mEisQRJssmSJT8CPhZdZa0AP6VCbWlXERKyGu0l09t0k50FiDiralpGhBxgA==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.2.tgz", + "integrity": "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==", "license": "MIT" }, "node_modules/which": { @@ -16841,25 +12177,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -16870,7 +12187,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -16881,9 +12197,9 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -16978,9 +12294,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -17041,6 +12357,19 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", diff --git a/ui/mobile/package.json b/ui/mobile/package.json index d00ded5e..19427906 100644 --- a/ui/mobile/package.json +++ b/ui/mobile/package.json @@ -8,6 +8,7 @@ "ios": "expo start --ios", "web": "expo start --web", "test": "jest", + "typecheck": "tsc --noEmit", "lint": "eslint ." }, "dependencies": { @@ -18,10 +19,10 @@ "@types/three": "^0.183.1", "axios": "^1.15.2", "expo": "~55.0.4", - "expo-status-bar": "~55.0.4", + "expo-status-bar": "~55.0.6", "react": "19.2.0", - "react-dom": "19.2.6", - "react-native": "0.85.2", + "react-dom": "19.2.0", + "react-native": "0.83.10", "react-native-gesture-handler": "~2.30.0", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.2", @@ -35,18 +36,21 @@ "zustand": "^5.0.12" }, "devDependencies": { + "@eslint/js": "10.0.1", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^13.3.3", - "@types/jest": "^30.0.0", + "@types/jest": "29.5.14", "@types/react": "~19.2.2", "@typescript-eslint/eslint-plugin": "^8.59.3", "@typescript-eslint/parser": "^8.56.1", "babel-preset-expo": "^55.0.10", "eslint": "^10.2.1", - "jest": "^30.2.0", + "eslint-plugin-react-hooks": "7.1.1", + "globals": "17.11.0", + "jest": "~29.7.0", "jest-expo": "^55.0.9", "prettier": "^3.8.3", - "react-native-worklets": "^0.7.4", + "react-native-worklets": "0.7.4", "typescript": "~5.9.2" }, "overrides": { diff --git a/ui/mobile/src/__tests__/__mocks__/reanimated.js b/ui/mobile/src/__tests__/__mocks__/reanimated.js new file mode 100644 index 00000000..3a83a452 --- /dev/null +++ b/ui/mobile/src/__tests__/__mocks__/reanimated.js @@ -0,0 +1,50 @@ +const ReactNative = require('react-native'); + +const identity = (value) => value; +const noop = () => undefined; +const createSharedValue = (initialValue) => ({ + value: initialValue, + get: () => initialValue, + set(nextValue) { + this.value = typeof nextValue === 'function' ? nextValue(this.value) : nextValue; + }, +}); +const evaluate = (updater) => updater(); +const createAnimatedComponent = (Component) => Component; + +const Easing = { + linear: identity, + ease: identity, + quad: identity, + cubic: identity, + in: identity, + out: identity, + inOut: identity, +}; + +const Animated = { + View: ReactNative.View, + Text: ReactNative.Text, + Image: ReactNative.Image, + ScrollView: ReactNative.ScrollView, + createAnimatedComponent, +}; + +module.exports = { + __esModule: true, + default: Animated, + Easing, + cancelAnimation: noop, + createAnimatedComponent, + interpolateColor: (_value, _input, output) => output[0], + runOnJS: identity, + useAnimatedProps: evaluate, + useAnimatedReaction: noop, + useAnimatedStyle: evaluate, + useDerivedValue: (updater) => createSharedValue(updater()), + useSharedValue: createSharedValue, + withRepeat: identity, + withSequence: (...values) => values[values.length - 1], + withSpring: identity, + withTiming: identity, +}; diff --git a/ui/mobile/src/__tests__/screens/MATScreen.test.tsx b/ui/mobile/src/__tests__/screens/MATScreen.test.tsx index e30e5c6c..579def3a 100644 --- a/ui/mobile/src/__tests__/screens/MATScreen.test.tsx +++ b/ui/mobile/src/__tests__/screens/MATScreen.test.tsx @@ -68,13 +68,13 @@ describe('MATScreen', () => { it('renders the connection banner', () => { const { MATScreen } = require('@/screens/MATScreen'); - const { getByText } = render( + const { getAllByText } = render( , ); // Simulated status maps to 'simulated' banner -> "SIMULATED DATA" - expect(getByText('SIMULATED DATA')).toBeTruthy(); + expect(getAllByText('SIMULATED DATA').length).toBeGreaterThan(0); }); it('shows simulation warning overlay when simulated and not acknowledged', () => { diff --git a/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx b/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx new file mode 100644 index 00000000..c4fd4f26 --- /dev/null +++ b/ui/mobile/src/__tests__/screens/NLOSScreen.test.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react-native'; +import { createSyntheticNlosFrame } from '@/services/nlos.service'; +import { createLiveNlosFrameFixture } from '@/testUtils/nlosFixtures'; +import { ThemeProvider } from '@/theme/ThemeContext'; + +const syntheticFrame = createSyntheticNlosFrame(0, 1_700_000_000_000); +const mockNlosResult: Record = { + frame: syntheticFrame, + freshness: 'fresh' as const, + streamStatus: 'synthetic_replay' as const, + lastRejectedReason: null, + rejectedFrameCount: 0, + liveCredentialAvailable: false, + configureCredential: jest.fn(() => true), + forgetCredential: jest.fn(), + startReplay: jest.fn(), + connectLive: jest.fn(), +}; + +jest.mock('@/hooks/useNlosStream', () => ({ + useNlosStream: () => mockNlosResult, +})); + +jest.mock('react-native-svg', () => { + const { View, Text } = require('react-native'); + return { + __esModule: true, + default: View, + Circle: View, + Ellipse: View, + Line: View, + Polygon: View, + Rect: View, + Text, + }; +}); + +describe('NLOSScreen', () => { + beforeEach(() => { + Object.assign(mockNlosResult, { + frame: syntheticFrame, + freshness: 'fresh', + streamStatus: 'synthetic_replay', + lastRejectedReason: null, + rejectedFrameCount: 0, + liveCredentialAvailable: false, + }); + mockNlosResult.configureCredential.mockClear(); + mockNlosResult.forgetCredential.mockClear(); + }); + + it('renders the RuView NLOS screen and iPhone API boundary', () => { + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + expect(screen.getByText('RuView NLOS')).toBeTruthy(); + expect(screen.getByText(/does not access raw iPhone LiDAR timing data/)).toBeTruthy(); + }); + + it('always watermarks synthetic replay', () => { + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + expect(screen.getByTestId('nlos-synthetic-watermark')).toBeTruthy(); + expect(screen.getByTestId('nlos-provenance-badge').props.children).toBe('SYNTHETIC'); + }); + + it('does not enable live without an ephemeral credential', () => { + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + const button = screen.getByRole('button', { name: 'CONNECT AUTHENTICATED LIVE' }); + expect(button.props.accessibilityState?.disabled ?? button.props.disabled).toBeTruthy(); + expect(screen.getByText(/never stored by this client/)).toBeTruthy(); + }); + + it('keeps a manually entered pairing credential bounded and masked', () => { + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + + const input = screen.getByTestId('nlos-credential-input'); + expect(input.props.secureTextEntry).toBe(true); + expect(input.props.maxLength).toBe(512); + const unlock = screen.getByRole('button', { name: 'UNLOCK AUTHENTICATED LIVE' }); + expect(unlock.props.accessibilityState?.disabled ?? unlock.props.disabled).toBeTruthy(); + + const token = 'p'.repeat(32); + fireEvent.changeText(input, token); + fireEvent.press(screen.getByRole('button', { name: 'UNLOCK AUTHENTICATED LIVE' })); + expect(mockNlosResult.configureCredential).toHaveBeenCalledWith(token); + expect(screen.getByTestId('nlos-credential-input').props.value).toBe(''); + }); + + it('renders unknown evidence without a live or synthetic claim', () => { + Object.assign(mockNlosResult, { frame: null, freshness: 'unknown', streamStatus: 'idle' }); + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + expect(screen.getByTestId('nlos-provenance-badge').props.children).toBe('UNKNOWN'); + expect(screen.queryByTestId('nlos-synthetic-watermark')).toBeNull(); + expect(screen.getByText(/Unknown evidence is never promoted to live/)).toBeTruthy(); + }); + + it('keeps stale measured frames visibly stale', () => { + Object.assign(mockNlosResult, { + frame: createLiveNlosFrameFixture(), + freshness: 'stale', + streamStatus: 'error', + liveCredentialAvailable: true, + }); + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + expect(screen.getByTestId('nlos-stale-overlay')).toBeTruthy(); + expect(screen.getByTestId('nlos-freshness-badge').props.children).toBe('STALE'); + expect(screen.getByTestId('nlos-track-count').props.children).toBe(0); + expect(screen.getByTestId('nlos-mean-confidence').props.children).toBe('N/A'); + expect(screen.queryByTestId('nlos-synthetic-watermark')).toBeNull(); + }); + + it('never draws or counts unknown target hypotheses', () => { + const live = createLiveNlosFrameFixture(); + Object.assign(mockNlosResult, { + frame: { + ...live, + tracks: [{ ...live.tracks[0], state: 'unknown' }], + }, + freshness: 'fresh', + streamStatus: 'live', + }); + const { NLOSScreen } = require('@/screens/NLOSScreen'); + render(); + expect(screen.getByTestId('nlos-track-count').props.children).toBe(0); + expect(screen.getByTestId('nlos-mean-confidence').props.children).toBe('N/A'); + expect(screen.queryByText(live.tracks[0].trackId)).toBeNull(); + }); +}); diff --git a/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx b/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx index c21e3153..77870b5e 100644 --- a/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx +++ b/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx @@ -25,6 +25,7 @@ describe('SettingsScreen', () => { beforeEach(() => { useSettingsStore.setState({ serverUrl: 'http://localhost:3000', + nlosServerUrl: 'http://localhost:3000', rssiScanEnabled: false, theme: 'system', alertSoundEnabled: true, diff --git a/ui/mobile/src/__tests__/screens/VitalsScreen.test.tsx b/ui/mobile/src/__tests__/screens/VitalsScreen.test.tsx index 3a725c31..c188a183 100644 --- a/ui/mobile/src/__tests__/screens/VitalsScreen.test.tsx +++ b/ui/mobile/src/__tests__/screens/VitalsScreen.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react-native'; import { ThemeProvider } from '@/theme/ThemeContext'; +import { usePoseStore } from '@/stores/poseStore'; jest.mock('@/hooks/usePoseStream', () => ({ usePoseStream: () => ({ @@ -26,6 +27,10 @@ jest.mock('react-native-svg', () => { }); describe('VitalsScreen', () => { + beforeEach(() => { + usePoseStore.setState({ connectionStatus: 'simulated', isSimulated: true }); + }); + it('module exports VitalsScreen as default', () => { const mod = require('@/screens/VitalsScreen'); expect(mod.default).toBeDefined(); diff --git a/ui/mobile/src/__tests__/services/api.service.test.ts b/ui/mobile/src/__tests__/services/api.service.test.ts index bf54ad50..e146b1b9 100644 --- a/ui/mobile/src/__tests__/services/api.service.test.ts +++ b/ui/mobile/src/__tests__/services/api.service.test.ts @@ -129,7 +129,7 @@ describe('ApiService', () => { isAxiosError: true, }; mockRequest.mockRejectedValue(axiosError); - (mockAxios.isAxiosError as jest.Mock).mockReturnValue(true); + (mockAxios.isAxiosError as unknown as jest.Mock).mockReturnValue(true); await expect(apiService.get('/test')).rejects.toEqual( expect.objectContaining({ @@ -142,7 +142,7 @@ describe('ApiService', () => { it('normalizes generic Error', async () => { mockRequest.mockRejectedValue(new Error('network timeout')); - (mockAxios.isAxiosError as jest.Mock).mockReturnValue(false); + (mockAxios.isAxiosError as unknown as jest.Mock).mockReturnValue(false); await expect(apiService.get('/test')).rejects.toEqual( expect.objectContaining({ message: 'network timeout' }), @@ -151,7 +151,7 @@ describe('ApiService', () => { it('normalizes unknown error', async () => { mockRequest.mockRejectedValue('string error'); - (mockAxios.isAxiosError as jest.Mock).mockReturnValue(false); + (mockAxios.isAxiosError as unknown as jest.Mock).mockReturnValue(false); await expect(apiService.get('/test')).rejects.toEqual( expect.objectContaining({ message: 'Unknown error' }), @@ -163,7 +163,7 @@ describe('ApiService', () => { it('retries up to 2 times on failure then throws', async () => { const error = new Error('fail'); mockRequest.mockRejectedValue(error); - (mockAxios.isAxiosError as jest.Mock).mockReturnValue(false); + (mockAxios.isAxiosError as unknown as jest.Mock).mockReturnValue(false); await expect(apiService.get('/flaky')).rejects.toEqual( expect.objectContaining({ message: 'fail' }), diff --git a/ui/mobile/src/__tests__/services/nlos.service.test.ts b/ui/mobile/src/__tests__/services/nlos.service.test.ts new file mode 100644 index 00000000..8e6bb789 --- /dev/null +++ b/ui/mobile/src/__tests__/services/nlos.service.test.ts @@ -0,0 +1,285 @@ +import { + NLOS_AUTHENTICATED_SCHEMA, + NLOS_TICKET_SCHEMA, + NlosService, + configureNlosBearerToken, + createSyntheticNlosFrame, + hasConfiguredNlosBearerToken, + type NlosServiceDependencies, +} from '@/services/nlos.service'; +import { createLiveNlosFrameFixture } from '@/testUtils/nlosFixtures'; +import { NLOS_MAX_MESSAGE_BYTES } from '@/types/nlos'; + +class MockSocket { + readyState = 0; + onopen: (() => void) | null = null; + onmessage: ((event: { data: unknown }) => void) | null = null; + onerror: (() => void) | null = null; + onclose: ((event: { code: number }) => void) | null = null; + close = jest.fn(); +} + +const NOW = 1_700_000_000_100; +const BEARER_TOKEN = 'e'.repeat(32); + +const createHarness = () => { + const socket = new MockSocket(); + const fetchMock = jest.fn(async () => ({ + ok: true, + status: 200, + text: async () => JSON.stringify({ + schema: NLOS_TICKET_SCHEMA, + webSocketUrl: `wss://ruview.example/api/v1/nlos/ws?ticket=${'a'.repeat(64)}`, + expiresAtUnixMs: NOW + 30_000, + }), + })); + const dependencies: NlosServiceDependencies = { + fetch: fetchMock, + createWebSocket: jest.fn(() => socket), + now: jest.fn(() => NOW), + setInterval: globalThis.setInterval.bind(globalThis), + clearInterval: globalThis.clearInterval.bind(globalThis), + setTimeout: globalThis.setTimeout.bind(globalThis), + clearTimeout: globalThis.clearTimeout.bind(globalThis), + }; + return { service: new NlosService(dependencies), socket, fetchMock, dependencies }; +}; + +const authenticate = (socket: MockSocket) => { + socket.onmessage?.({ + data: JSON.stringify({ + schema: NLOS_AUTHENTICATED_SCHEMA, + sessionId: 'live-session-1', + expiresAtUnixMs: NOW + 25_000, + }), + }); +}; + +describe('NlosService', () => { + afterEach(() => { + configureNlosBearerToken(null); + }); + + it('exchanges a transport Bearer token for a one time socket before accepting live frames', async () => { + const { service, socket, fetchMock } = createHarness(); + const listener = jest.fn(); + service.subscribe(listener); + + await expect(service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN })).resolves.toBe(true); + expect(fetchMock).toHaveBeenCalledWith( + 'https://ruview.example/api/v1/nlos/ws-ticket', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ Authorization: `Bearer ${BEARER_TOKEN}` }), + }), + ); + + authenticate(socket); + expect(service.getStatus()).toBe('live'); + const frame = createLiveNlosFrameFixture(); + socket.onmessage?.({ data: JSON.stringify(frame) }); + expect(listener).toHaveBeenCalledWith({ + frame, + channel: 'authenticated_stream', + receivedAtUnixMs: NOW, + }); + }); + + it('rejects data before the authenticated session acknowledgement', async () => { + const { service, socket } = createHarness(); + const rejected = jest.fn(); + service.subscribeRejected(rejected); + await service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN }); + + socket.onmessage?.({ data: JSON.stringify(createLiveNlosFrameFixture()) }); + expect(rejected).toHaveBeenCalledWith('unauthenticated'); + expect(socket.close).toHaveBeenCalledWith(1008, 'authentication required'); + }); + + it('accepts authenticated synthetic server frames without promoting their evidence', async () => { + const { service, socket } = createHarness(); + const listener = jest.fn(); + service.subscribe(listener); + await service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN }); + authenticate(socket); + const frame = { + ...createSyntheticNlosFrame(3, NOW), + sessionId: 'live-session-1', + provenance: { + ...createSyntheticNlosFrame(3, NOW).provenance, + histogramPreserved: true, + }, + }; + socket.onmessage?.({ data: JSON.stringify(frame) }); + expect(listener).toHaveBeenCalledWith({ + frame, + channel: 'authenticated_stream', + receivedAtUnixMs: NOW, + }); + expect(frame.evidenceLevel).toBe('l0_synthetic'); + }); + + it('bounds the authenticated socket handshake to five seconds', async () => { + jest.useFakeTimers(); + try { + const { service, socket } = createHarness(); + const rejected = jest.fn(); + service.subscribeRejected(rejected); + await service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN }); + jest.advanceTimersByTime(5_000); + expect(rejected).toHaveBeenCalledWith('unauthenticated'); + expect(socket.close).toHaveBeenCalledWith(1008, 'authentication timeout'); + expect(service.getStatus()).toBe('error'); + service.disconnect(); + } finally { + jest.useRealTimers(); + } + }); + + it('expires an authenticated session even when the socket is idle', async () => { + jest.useFakeTimers(); + try { + const { service, socket } = createHarness(); + const rejected = jest.fn(); + service.subscribeRejected(rejected); + await service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN }); + authenticate(socket); + expect(service.getStatus()).toBe('live'); + jest.advanceTimersByTime(25_000); + expect(rejected).toHaveBeenCalledWith('unauthenticated'); + expect(socket.close).toHaveBeenCalledWith(1008, 'session expired'); + expect(service.getStatus()).toBe('error'); + service.disconnect(); + } finally { + jest.useRealTimers(); + } + }); + + it('rejects duplicate and out of order sequences', async () => { + const { service, socket } = createHarness(); + const listener = jest.fn(); + const rejected = jest.fn(); + service.subscribe(listener); + service.subscribeRejected(rejected); + await service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN }); + authenticate(socket); + + socket.onmessage?.({ data: JSON.stringify(createLiveNlosFrameFixture({ sequence: 5 })) }); + socket.onmessage?.({ data: JSON.stringify(createLiveNlosFrameFixture({ sequence: 5 })) }); + socket.onmessage?.({ data: JSON.stringify(createLiveNlosFrameFixture({ sequence: 4 })) }); + expect(listener).toHaveBeenCalledTimes(1); + expect(rejected).toHaveBeenCalledTimes(2); + expect(rejected).toHaveBeenLastCalledWith('out_of_order'); + }); + + it('bounds messages and rejects binary payloads', async () => { + const { service, socket } = createHarness(); + const rejected = jest.fn(); + service.subscribeRejected(rejected); + await service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: BEARER_TOKEN }); + authenticate(socket); + + socket.onmessage?.({ data: `{"padding":"${'x'.repeat(NLOS_MAX_MESSAGE_BYTES)}"}` }); + socket.onmessage?.({ data: new Uint8Array([1, 2, 3]) }); + expect(rejected).toHaveBeenCalledWith('message_too_large'); + expect(rejected).toHaveBeenCalledWith('unsupported_binary'); + }); + + it('rejects remote cleartext server URLs', async () => { + const { service, fetchMock } = createHarness(); + await expect(service.connectLive({ serverUrl: 'http://ruview.example', bearerToken: BEARER_TOKEN })).resolves.toBe(false); + expect(fetchMock).not.toHaveBeenCalled(); + expect(service.getStatus()).toBe('error'); + }); + + it('rejects a ticket that redirects the socket to another authority', async () => { + const { service, fetchMock, dependencies } = createHarness(); + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + text: async () => JSON.stringify({ + schema: NLOS_TICKET_SCHEMA, + webSocketUrl: `wss://attacker.example/api/v1/nlos/ws?ticket=${'a'.repeat(64)}`, + expiresAtUnixMs: NOW + 10_000, + }), + }); + await expect(service.connectLive({ + serverUrl: 'https://ruview.example', + bearerToken: BEARER_TOKEN, + })).resolves.toBe(false); + expect(dependencies.createWebSocket).not.toHaveBeenCalled(); + }); + + it('enforces the 32 to 512 character ephemeral credential bound', async () => { + const short = createHarness(); + await expect(short.service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: 'x'.repeat(31) })).resolves.toBe(false); + expect(short.fetchMock).not.toHaveBeenCalled(); + + const long = createHarness(); + await expect(long.service.connectLive({ serverUrl: 'https://ruview.example', bearerToken: 'x'.repeat(513) })).resolves.toBe(false); + expect(long.fetchMock).not.toHaveBeenCalled(); + }); + + it('keeps configured credentials memory-only and applies the same length bound', () => { + expect(configureNlosBearerToken('x'.repeat(31))).toBe(false); + expect(hasConfiguredNlosBearerToken()).toBe(false); + expect(configureNlosBearerToken(`${'x'.repeat(31)} `)).toBe(false); + expect(configureNlosBearerToken('x'.repeat(513))).toBe(false); + expect(hasConfiguredNlosBearerToken()).toBe(false); + + expect(configureNlosBearerToken('x'.repeat(32))).toBe(true); + expect(hasConfiguredNlosBearerToken()).toBe(true); + expect(configureNlosBearerToken(null)).toBe(true); + expect(hasConfiguredNlosBearerToken()).toBe(false); + }); + + it('emits bounded, visibly synthetic deterministic replay frames', () => { + jest.useFakeTimers(); + try { + const { service } = createHarness(); + const listener = jest.fn(); + service.subscribe(listener); + service.startDeterministicReplay(1_000); + expect(service.getStatus()).toBe('synthetic_replay'); + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.calls[0][0]).toEqual({ + frame: createSyntheticNlosFrame(0, NOW, 30), + channel: 'deterministic_replay', + receivedAtUnixMs: NOW, + }); + jest.advanceTimersByTime(34); + expect(listener).toHaveBeenCalledTimes(2); + service.disconnect(); + } finally { + jest.useRealTimers(); + } + }); + + it('rejects unsafe synthetic sequences and bounds non-finite replay rates', () => { + expect(() => createSyntheticNlosFrame(Number.MAX_SAFE_INTEGER + 1, NOW)).toThrow(RangeError); + expect(() => createSyntheticNlosFrame(0, Number.MAX_SAFE_INTEGER)).toThrow(RangeError); + + jest.useFakeTimers(); + try { + const { service } = createHarness(); + const listener = jest.fn(); + service.subscribe(listener); + service.startDeterministicReplay(Number.NaN); + jest.advanceTimersByTime(66); + expect(listener).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(1); + expect(listener).toHaveBeenCalledTimes(2); + service.disconnect(); + } finally { + jest.useRealTimers(); + } + }); + + it('reports velocity in metres per second at the selected replay rate', () => { + const slow = createSyntheticNlosFrame(0, NOW, 10).tracks[0].velocityMps; + const fast = createSyntheticNlosFrame(0, NOW, 20).tracks[0].velocityMps; + expect(fast.x).toBeCloseTo(slow.x * 2, 6); + expect(fast.y).toBeCloseTo(slow.y * 2, 6); + expect(fast.z).toBeCloseTo(slow.z * 2, 6); + }); +}); diff --git a/ui/mobile/src/__tests__/services/nlos.validation.test.ts b/ui/mobile/src/__tests__/services/nlos.validation.test.ts new file mode 100644 index 00000000..87ab20e3 --- /dev/null +++ b/ui/mobile/src/__tests__/services/nlos.validation.test.ts @@ -0,0 +1,126 @@ +import { parseNlosTrackFrame, utf8ByteLength, validateNlosTrackFrame } from '@/services/nlos.validation'; +import { createSyntheticNlosFrame } from '@/services/nlos.service'; +import { createLiveNlosFrameFixture } from '@/testUtils/nlosFixtures'; +import { NLOS_MAX_MESSAGE_BYTES, NLOS_MAX_TRACKS } from '@/types/nlos'; + +describe('NLOS track validation', () => { + it('accepts the canonical measured live contract', () => { + const frame = createLiveNlosFrameFixture(); + expect(validateNlosTrackFrame(frame)).toEqual({ ok: true, value: frame }); + expect(parseNlosTrackFrame(JSON.stringify(frame))).toEqual({ ok: true, value: frame }); + }); + + it('rejects malformed JSON and messages over the 256 KiB limit', () => { + expect(parseNlosTrackFrame('{bad-json')).toEqual({ ok: false, reason: 'malformed_json' }); + const oversized = `{"padding":"${'x'.repeat(NLOS_MAX_MESSAGE_BYTES)}"}`; + expect(utf8ByteLength(oversized)).toBeGreaterThan(NLOS_MAX_MESSAGE_BYTES); + expect(parseNlosTrackFrame(oversized)).toEqual({ ok: false, reason: 'message_too_large' }); + }); + + it('rejects excessive track counts and spatial bounds', () => { + const oneTrack = createLiveNlosFrameFixture().tracks[0]; + const tooMany = createLiveNlosFrameFixture({ + tracks: Array.from({ length: NLOS_MAX_TRACKS + 1 }, (_, index) => ({ + ...oneTrack, + trackId: `target-${index}`, + })), + }); + expect(validateNlosTrackFrame(tooMany)).toEqual({ ok: false, reason: 'invalid_bounds' }); + + const outOfBounds = createLiveNlosFrameFixture({ + tracks: [{ ...oneTrack, positionM: { x: 100.01, y: 0, z: 0 } }], + }); + expect(validateNlosTrackFrame(outOfBounds)).toEqual({ ok: false, reason: 'invalid_shape' }); + }); + + it('never promotes depth only or histogram free data to live NLOS', () => { + const frame = createLiveNlosFrameFixture({ + provenance: { + ...createLiveNlosFrameFixture().provenance, + transientKind: 'depth_only', + histogramPreserved: false, + }, + }); + expect(validateNlosTrackFrame(frame)).toEqual({ ok: false, reason: 'invalid_provenance' }); + expect(validateNlosTrackFrame(createLiveNlosFrameFixture({ + provenance: { + ...createLiveNlosFrameFixture().provenance, + transport: 'replay', + }, + }))).toEqual({ ok: false, reason: 'invalid_provenance' }); + }); + + it('rejects captured replay evidence when timing histograms were discarded', () => { + const live = createLiveNlosFrameFixture(); + const replay = { + ...live, + source: 'replay' as const, + provenance: { + ...live.provenance, + transientKind: 'replay' as const, + histogramPreserved: false, + transport: 'replay' as const, + }, + }; + + expect(validateNlosTrackFrame(replay)).toEqual({ + ok: false, + reason: 'invalid_provenance', + }); + expect(validateNlosTrackFrame({ + ...replay, + provenance: { ...replay.provenance, histogramPreserved: true }, + }).ok).toBe(true); + }); + + it('accepts deterministic synthetic frames only with L0 and the zero calibration hash', () => { + const synthetic = createSyntheticNlosFrame(0, 1_700_000_000_000); + expect(validateNlosTrackFrame(synthetic).ok).toBe(true); + expect(validateNlosTrackFrame({ ...synthetic, evidenceLevel: 'l1_measured' })).toEqual({ + ok: false, + reason: 'invalid_provenance', + }); + expect(validateNlosTrackFrame({ ...synthetic, calibrationHash: 'b'.repeat(64) })).toEqual({ + ok: false, + reason: 'invalid_provenance', + }); + expect(validateNlosTrackFrame({ + ...synthetic, + provenance: { ...synthetic.provenance, histogramPreserved: true }, + }).ok).toBe(true); + expect(validateNlosTrackFrame({ + ...synthetic, + provenance: { ...synthetic.provenance, transientKind: 'raw_histogram' }, + })).toEqual({ ok: false, reason: 'invalid_provenance' }); + }); + + it('enforces calibrated hashes, unique tracks, and normalized modality weights', () => { + const frame = createLiveNlosFrameFixture(); + expect(validateNlosTrackFrame({ ...frame, calibrationHash: '0'.repeat(64) })).toEqual({ + ok: false, + reason: 'invalid_provenance', + }); + expect(validateNlosTrackFrame({ ...frame, tracks: [frame.tracks[0], frame.tracks[0]] })).toEqual({ + ok: false, + reason: 'invalid_provenance', + }); + expect(validateNlosTrackFrame({ + ...frame, + tracks: [{ + ...frame.tracks[0], + modalityContributions: { lidar: 0.8, csi: 0.8 }, + }], + })).toEqual({ ok: false, reason: 'invalid_shape' }); + expect(validateNlosTrackFrame({ ...frame, evidenceLevel: 'l3_corroborated' })).toEqual({ + ok: false, + reason: 'invalid_provenance', + }); + }); + + it('rejects unknown fields for a schema version instead of interpreting them ambiguously', () => { + expect(validateNlosTrackFrame({ ...createLiveNlosFrameFixture(), trustMe: true })).toEqual({ + ok: false, + reason: 'invalid_shape', + }); + }); +}); diff --git a/ui/mobile/src/__tests__/services/ws.service.test.ts b/ui/mobile/src/__tests__/services/ws.service.test.ts index 5342b940..16c1bf60 100644 --- a/ui/mobile/src/__tests__/services/ws.service.test.ts +++ b/ui/mobile/src/__tests__/services/ws.service.test.ts @@ -69,7 +69,7 @@ describe('WsService', () => { // Test with port 3000 ws.connect('http://192.168.1.10:3000'); - expect(capturedUrls[capturedUrls.length - 1]).toBe('ws://192.168.1.10:3000/ws/sensing'); + expect(capturedUrls[capturedUrls.length - 1]).toBe('ws://192.168.1.10:3000/api/v1/stream/pose'); // Clean up, create another service ws.disconnect(); @@ -77,19 +77,19 @@ describe('WsService', () => { // Test with port 8080 ws2.connect('http://myserver.local:8080'); - expect(capturedUrls[capturedUrls.length - 1]).toBe('ws://myserver.local:8080/ws/sensing'); + expect(capturedUrls[capturedUrls.length - 1]).toBe('ws://myserver.local:8080/api/v1/stream/pose'); ws2.disconnect(); // Test HTTPS -> WSS upgrade (port 443 is default for HTTPS so host drops it) const ws3 = createWsService(); ws3.connect('https://secure.example.com:443'); - expect(capturedUrls[capturedUrls.length - 1]).toBe('wss://secure.example.com/ws/sensing'); + expect(capturedUrls[capturedUrls.length - 1]).toBe('wss://secure.example.com/api/v1/stream/pose'); ws3.disconnect(); // Test WSS input const ws4 = createWsService(); ws4.connect('wss://secure.example.com'); - expect(capturedUrls[capturedUrls.length - 1]).toBe('wss://secure.example.com/ws/sensing'); + expect(capturedUrls[capturedUrls.length - 1]).toBe('wss://secure.example.com/api/v1/stream/pose'); ws4.disconnect(); // Verify port 3001 is NOT hardcoded anywhere diff --git a/ui/mobile/src/__tests__/stores/nlosStore.test.ts b/ui/mobile/src/__tests__/stores/nlosStore.test.ts new file mode 100644 index 00000000..76df7527 --- /dev/null +++ b/ui/mobile/src/__tests__/stores/nlosStore.test.ts @@ -0,0 +1,81 @@ +import { createSyntheticNlosFrame } from '@/services/nlos.service'; +import { useNlosStore } from '@/stores/nlosStore'; +import { createLiveNlosFrameFixture } from '@/testUtils/nlosFixtures'; +import { NLOS_STALE_AFTER_MS } from '@/types/nlos'; + +const NOW = 1_700_000_000_100; + +describe('useNlosStore', () => { + beforeEach(() => useNlosStore.getState().reset()); + + it('accepts authenticated live frames and starts fresh', () => { + const frame = createLiveNlosFrameFixture(); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW }); + expect(useNlosStore.getState()).toMatchObject({ frame, freshness: 'fresh', rejectedFrameCount: 0 }); + }); + + it('rejects a live frame delivered over a replay channel', () => { + useNlosStore.getState().ingestFrame({ + frame: createLiveNlosFrameFixture(), + channel: 'deterministic_replay', + receivedAtUnixMs: NOW, + }); + expect(useNlosStore.getState()).toMatchObject({ + frame: null, + freshness: 'unknown', + lastRejectedReason: 'unauthenticated', + rejectedFrameCount: 1, + }); + }); + + it('rejects replayed sequence numbers in the same session', () => { + const frame = createLiveNlosFrameFixture({ sequence: 8 }); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW }); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW + 1 }); + expect(useNlosStore.getState().rejectedFrameCount).toBe(1); + expect(useNlosStore.getState().lastRejectedReason).toBe('out_of_order'); + }); + + it('clears fresh frames immediately when they become stale', () => { + const frame = createLiveNlosFrameFixture({ expiresAtUnixMs: NOW + 4_900 }); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW }); + useNlosStore.getState().refreshFreshness(NOW + NLOS_STALE_AFTER_MS + 1); + expect(useNlosStore.getState()).toMatchObject({ frame: null, freshness: 'stale' }); + }); + + it('fails closed on wall clock rollback', () => { + const frame = createLiveNlosFrameFixture(); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW }); + useNlosStore.getState().refreshFreshness(NOW - 1); + expect(useNlosStore.getState()).toMatchObject({ frame: null, freshness: 'stale' }); + }); + + it('clears a previously accepted frame when transport validation rejects input', () => { + const frame = createLiveNlosFrameFixture(); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW }); + useNlosStore.getState().recordRejection('malformed_json'); + expect(useNlosStore.getState()).toMatchObject({ + frame: null, + freshness: 'unknown', + lastRejectedReason: 'malformed_json', + }); + }); + + it('clears a previously accepted frame immediately when transport closes', () => { + const frame = createLiveNlosFrameFixture(); + useNlosStore.getState().ingestFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: NOW }); + useNlosStore.getState().setStreamStatus('error'); + expect(useNlosStore.getState()).toMatchObject({ + frame: null, + freshness: 'unknown', + streamStatus: 'error', + }); + }); + + it('keeps synthetic replay explicitly synthetic', () => { + const frame = createSyntheticNlosFrame(0, NOW); + useNlosStore.getState().ingestFrame({ frame, channel: 'deterministic_replay', receivedAtUnixMs: NOW }); + expect(useNlosStore.getState().frame?.source).toBe('synthetic'); + expect(useNlosStore.getState().frame?.evidenceLevel).toBe('l0_synthetic'); + }); +}); diff --git a/ui/mobile/src/__tests__/stores/settingsStore.test.ts b/ui/mobile/src/__tests__/stores/settingsStore.test.ts index 7f2c78e6..2e09991f 100644 --- a/ui/mobile/src/__tests__/stores/settingsStore.test.ts +++ b/ui/mobile/src/__tests__/stores/settingsStore.test.ts @@ -5,6 +5,7 @@ describe('useSettingsStore', () => { // Reset to defaults by manually setting all values useSettingsStore.setState({ serverUrl: 'http://localhost:3000', + nlosServerUrl: 'http://localhost:3000', rssiScanEnabled: false, theme: 'system', alertSoundEnabled: true, @@ -41,6 +42,29 @@ describe('useSettingsStore', () => { }); }); + describe('setNlosServerUrl', () => { + it('updates NLOS independently from the CSI server URL', () => { + useSettingsStore.getState().setNlosServerUrl('https://nlos.example'); + expect(useSettingsStore.getState().nlosServerUrl).toBe('https://nlos.example'); + expect(useSettingsStore.getState().serverUrl).toBe('http://localhost:3000'); + }); + + it('never persists credentials or URL components outside the server origin', () => { + const initial = useSettingsStore.getState().nlosServerUrl; + for (const unsafe of [ + 'https://user:secret@nlos.example', + 'https://nlos.example/path', + 'https://nlos.example?token=secret', + 'https://nlos.example#secret', + ]) { + useSettingsStore.getState().setNlosServerUrl(unsafe); + expect(useSettingsStore.getState().nlosServerUrl).toBe(initial); + } + useSettingsStore.getState().setNlosServerUrl('https://nlos.example:443/'); + expect(useSettingsStore.getState().nlosServerUrl).toBe('https://nlos.example'); + }); + }); + describe('setRssiScanEnabled', () => { it('toggles to true', () => { useSettingsStore.getState().setRssiScanEnabled(true); diff --git a/ui/mobile/src/components/SparklineChart.tsx b/ui/mobile/src/components/SparklineChart.tsx index 890dc9a7..41be7b67 100644 --- a/ui/mobile/src/components/SparklineChart.tsx +++ b/ui/mobile/src/components/SparklineChart.tsx @@ -17,7 +17,7 @@ export const SparklineChart = ({ height = defaultHeight, style, }: SparklineChartProps) => { - const normalizedData = data.length > 0 ? data : [0]; + const normalizedData = useMemo(() => (data.length > 0 ? data : [0]), [data]); const chartData = useMemo( () => @@ -28,14 +28,11 @@ export const SparklineChart = ({ [normalizedData], ); - const yValues = normalizedData.map((value) => Number(value) || 0); - const yMin = Math.min(...yValues); - const yMax = Math.max(...yValues); - const yPadding = yMax - yMin === 0 ? 1 : (yMax - yMin) * 0.2; - return ( { + const nlosServerUrl = useSettingsStore((state) => state.nlosServerUrl); + const frame = useNlosStore((state) => state.frame); + const freshness = useNlosStore((state) => state.freshness); + const streamStatus = useNlosStore((state) => state.streamStatus); + const lastRejectedReason = useNlosStore((state) => state.lastRejectedReason); + const rejectedFrameCount = useNlosStore((state) => state.rejectedFrameCount); + const [liveCredentialAvailable, setLiveCredentialAvailable] = useState( + hasConfiguredNlosBearerToken, + ); + + useEffect(() => { + useNlosStore.getState().reset(); + const unsubscribeFrame = nlosService.subscribe((event) => { + useNlosStore.getState().ingestFrame(event); + }); + const unsubscribeStatus = nlosService.subscribeStatus((status) => { + useNlosStore.getState().setStreamStatus(status); + }); + const unsubscribeRejected = nlosService.subscribeRejected((reason) => { + useNlosStore.getState().recordRejection(reason); + }); + const freshnessTimer = setInterval(() => { + useNlosStore.getState().refreshFreshness(Date.now()); + }, FRESHNESS_POLL_MS); + + if (hasConfiguredNlosBearerToken()) { + void nlosService.connectConfiguredLive(nlosServerUrl); + } + + return () => { + clearInterval(freshnessTimer); + unsubscribeFrame(); + unsubscribeStatus(); + unsubscribeRejected(); + nlosService.disconnect(); + }; + }, [nlosServerUrl]); + + const startReplay = useCallback(() => { + useNlosStore.getState().reset(); + nlosService.startDeterministicReplay(); + }, []); + + const connectLive = useCallback(() => { + void nlosService.connectConfiguredLive(nlosServerUrl); + }, [nlosServerUrl]); + + const configureCredential = useCallback((token: string): boolean => { + const configured = configureNlosBearerToken(token); + if (configured) setLiveCredentialAvailable(true); + return configured; + }, []); + + const forgetCredential = useCallback(() => { + configureNlosBearerToken(null); + setLiveCredentialAvailable(false); + nlosService.disconnect(); + useNlosStore.getState().reset(); + }, []); + + return { + frame, + freshness, + streamStatus, + lastRejectedReason, + rejectedFrameCount, + liveCredentialAvailable, + configureCredential, + forgetCredential, + startReplay, + connectLive, + }; +}; diff --git a/ui/mobile/src/navigation/MainTabs.tsx b/ui/mobile/src/navigation/MainTabs.tsx index ee323381..e18f2b12 100644 --- a/ui/mobile/src/navigation/MainTabs.tsx +++ b/ui/mobile/src/navigation/MainTabs.tsx @@ -56,6 +56,7 @@ const wrapLazy = ( }; const LiveScreen = wrapLazy(() => import('../screens/LiveScreen'), 'Live'); +const NLOSScreen = wrapLazy(() => import('../screens/NLOSScreen'), 'NLOS'); const VitalsScreen = wrapLazy(() => import('../screens/VitalsScreen'), 'Vitals'); const ZonesScreen = wrapLazy(() => import('../screens/ZonesScreen'), 'Zones'); const MATScreen = wrapLazy(() => import('../screens/MATScreen'), 'MAT'); @@ -65,6 +66,8 @@ const toIconName = (routeName: keyof MainTabsParamList) => { switch (routeName) { case 'Live': return 'wifi'; + case 'NLOS': + return 'scan'; case 'Vitals': return 'heart'; case 'Zones': @@ -80,6 +83,7 @@ const toIconName = (routeName: keyof MainTabsParamList) => { const screens: ReadonlyArray<{ name: keyof MainTabsParamList; component: React.ComponentType }> = [ { name: 'Live', component: LiveScreen }, + { name: 'NLOS', component: NLOSScreen }, { name: 'Vitals', component: VitalsScreen }, { name: 'Zones', component: ZonesScreen }, { name: 'MAT', component: MATScreen }, diff --git a/ui/mobile/src/navigation/types.ts b/ui/mobile/src/navigation/types.ts index 2c72c462..0b4d60d5 100644 --- a/ui/mobile/src/navigation/types.ts +++ b/ui/mobile/src/navigation/types.ts @@ -4,6 +4,7 @@ export type RootStackParamList = { export type MainTabsParamList = { Live: undefined; + NLOS: undefined; Vitals: undefined; Zones: undefined; MAT: undefined; diff --git a/ui/mobile/src/screens/LiveScreen/index.tsx b/ui/mobile/src/screens/LiveScreen/index.tsx index 67229e5f..e18bec30 100644 --- a/ui/mobile/src/screens/LiveScreen/index.tsx +++ b/ui/mobile/src/screens/LiveScreen/index.tsx @@ -43,7 +43,7 @@ const WebLiveViewer = ({ frame, onReady, onFps, onError }: ViewerProps) => { return ; }; -const NativeLiveViewer = ({ frame, onReady, onFps, onError }: ViewerProps) => { +const NativeLiveViewer = ({ onReady, onFps, onError }: ViewerProps) => { const webViewRef = useRef(null); const [WVComponent, setWVComponent] = useState | null>(null); diff --git a/ui/mobile/src/screens/MATScreen/SimulationBanner.tsx b/ui/mobile/src/screens/MATScreen/SimulationBanner.tsx index 86b5c871..7cc07562 100644 --- a/ui/mobile/src/screens/MATScreen/SimulationBanner.tsx +++ b/ui/mobile/src/screens/MATScreen/SimulationBanner.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import { Animated, StyleSheet, Text, View } from 'react-native'; +import { Animated, StyleSheet, Text } from 'react-native'; interface Props { visible: boolean; diff --git a/ui/mobile/src/screens/NLOSScreen/HiddenTargetVisualization.tsx b/ui/mobile/src/screens/NLOSScreen/HiddenTargetVisualization.tsx new file mode 100644 index 00000000..7db3ae15 --- /dev/null +++ b/ui/mobile/src/screens/NLOSScreen/HiddenTargetVisualization.tsx @@ -0,0 +1,138 @@ +import React, { memo, useMemo } from 'react'; +import { View } from 'react-native'; +import Svg, { Circle, Ellipse, Line, Polygon, Rect, Text as SvgText } from 'react-native-svg'; +import { colors } from '@/theme/colors'; +import type { NlosFreshness, NlosTrack } from '@/types/nlos'; + +export type NlosViewMode = 'plan' | 'perspective'; + +interface HiddenTargetVisualizationProps { + tracks: NlosTrack[]; + freshness: NlosFreshness; + mode: NlosViewMode; + width: number; +} + +interface ProjectedTrack { + track: NlosTrack; + x: number; + y: number; + radiusX: number; + radiusY: number; + velocityX: number; + velocityY: number; +} + +const CANVAS_WIDTH = 360; +const CANVAS_HEIGHT = 260; + +const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)); + +const resolveTrackColor = (track: NlosTrack, freshness: NlosFreshness): string => { + if (freshness !== 'fresh' || track.state === 'unknown') return colors.muted; + if (track.state === 'degraded') return colors.warn; + return colors.accent; +}; + +const projectPlan = (track: NlosTrack): ProjectedTrack => { + const x = 180 + clamp(track.positionM.x, -6, 6) * 24; + const y = 232 - clamp(track.positionM.z, 0, 8) * 25; + return { + track, + x, + y, + radiusX: clamp(Math.sqrt(track.covarianceDiagonalM2.x) * 24, 5, 28), + radiusY: clamp(Math.sqrt(track.covarianceDiagonalM2.z) * 25, 5, 28), + velocityX: track.velocityMps.x * 10, + velocityY: -track.velocityMps.z * 10, + }; +}; + +const projectPerspective = (track: NlosTrack): ProjectedTrack => { + const position = track.positionM; + const x = 180 + (clamp(position.x, -6, 6) - clamp(position.z, 0, 8)) * 17; + const y = 205 + (clamp(position.x, -6, 6) + clamp(position.z, 0, 8)) * 6 - clamp(position.y, 0, 4) * 25; + return { + track, + x, + y, + radiusX: clamp(Math.sqrt(track.covarianceDiagonalM2.x) * 22, 5, 26), + radiusY: clamp(Math.sqrt(track.covarianceDiagonalM2.y + track.covarianceDiagonalM2.z) * 10, 4, 22), + velocityX: (track.velocityMps.x - track.velocityMps.z) * 8, + velocityY: (track.velocityMps.x + track.velocityMps.z - track.velocityMps.y) * 4, + }; +}; + +const PlanScene = () => ( + <> + + + + HIDDEN REGION + RELAY SURFACE + + + SENSOR + +); + +const PerspectiveScene = () => ( + <> + + + + + + + BEYOND RELAY PLANE + + +); + +export const HiddenTargetVisualization = memo(({ + tracks, + freshness, + mode, + width, +}: HiddenTargetVisualizationProps) => { + const projectedTracks = useMemo( + () => tracks.map(mode === 'plan' ? projectPlan : projectPerspective), + [mode, tracks], + ); + const displayWidth = Math.max(260, Math.min(width, 560)); + + return ( + + ); +}); + +HiddenTargetVisualization.displayName = 'HiddenTargetVisualization'; diff --git a/ui/mobile/src/screens/NLOSScreen/ProvenancePanel.tsx b/ui/mobile/src/screens/NLOSScreen/ProvenancePanel.tsx new file mode 100644 index 00000000..4e1aac0d --- /dev/null +++ b/ui/mobile/src/screens/NLOSScreen/ProvenancePanel.tsx @@ -0,0 +1,98 @@ +import { StyleSheet, View } from 'react-native'; +import { ThemedText } from '@/components/ThemedText'; +import { colors } from '@/theme/colors'; +import { spacing } from '@/theme/spacing'; +import type { NlosFreshness, NlosStreamStatus, NlosTrackFrame } from '@/types/nlos'; + +interface ProvenancePanelProps { + frame: NlosTrackFrame | null; + freshness: NlosFreshness; + streamStatus: NlosStreamStatus; +} + +const sourceLabel = (frame: NlosTrackFrame | null): string => { + if (!frame) return 'UNKNOWN'; + if (frame.source === 'synthetic') return 'SYNTHETIC'; + if (frame.source === 'replay') return 'REPLAY'; + return 'LIVE'; +}; + +const sourceColor = (frame: NlosTrackFrame | null): string => { + if (!frame) return colors.muted; + if (frame.source === 'synthetic') return colors.warn; + if (frame.source === 'replay') return colors.textSecondary; + return colors.success; +}; + +const humanize = (value: string) => value.replace(/_/g, ' ').toUpperCase(); + +const ProvenanceRow = ({ label, value }: { label: string; value: string }) => ( + + {label} + {value} + +); + +export const ProvenancePanel = ({ frame, freshness, streamStatus }: ProvenancePanelProps) => { + const label = sourceLabel(frame); + const accent = sourceColor(frame); + + return ( + + + + {label} + + + {freshness.toUpperCase()} + + + {humanize(streamStatus)} + + + + {frame ? ( + + + + + + + + ) : ( + + No validated frame is available. Unknown evidence is never promoted to live. + + )} + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderWidth: 1, + borderRadius: 12, + padding: spacing.md, + gap: spacing.md, + }, + badgeRow: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + gap: spacing.sm, + }, + badge: { + borderWidth: 1, + borderRadius: 999, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + }, + grid: { + gap: spacing.xs, + }, + provenanceRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + provenanceLabel: { width: 82 }, + provenanceValue: { flex: 1 }, +}); diff --git a/ui/mobile/src/screens/NLOSScreen/index.tsx b/ui/mobile/src/screens/NLOSScreen/index.tsx new file mode 100644 index 00000000..44fdaced --- /dev/null +++ b/ui/mobile/src/screens/NLOSScreen/index.tsx @@ -0,0 +1,223 @@ +import { useMemo, useState } from 'react'; +import { Pressable, ScrollView, StyleSheet, TextInput, useWindowDimensions, View } from 'react-native'; +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { useNlosStream } from '@/hooks/useNlosStream'; +import { colors } from '@/theme/colors'; +import { spacing } from '@/theme/spacing'; +import { HiddenTargetVisualization, type NlosViewMode } from './HiddenTargetVisualization'; +import { ProvenancePanel } from './ProvenancePanel'; + +const ViewModePicker = ({ value, onChange }: { value: NlosViewMode; onChange: (value: NlosViewMode) => void }) => ( + + {(['plan', 'perspective'] as const).map((option) => { + const selected = option === value; + return ( + onChange(option)} + style={[styles.pickerButton, selected && styles.pickerButtonSelected]} + > + + {option === 'plan' ? '2D PLAN' : '3D VIEW'} + + + ); + })} + +); + +export const NLOSScreen = () => { + const { + frame, + freshness, + streamStatus, + lastRejectedReason, + rejectedFrameCount, + liveCredentialAvailable, + configureCredential, + forgetCredential, + startReplay, + connectLive, + } = useNlosStream(); + const [viewMode, setViewMode] = useState('plan'); + const [credentialDraft, setCredentialDraft] = useState(''); + const [credentialError, setCredentialError] = useState(false); + const { width } = useWindowDimensions(); + const visualizationWidth = useMemo(() => width - spacing.md * 2, [width]); + const isSynthetic = frame?.source === 'synthetic'; + const visibleTracks = useMemo( + () => freshness === 'fresh' + ? frame?.tracks.filter((track) => track.state !== 'unknown') ?? [] + : [], + [frame, freshness], + ); + const credentialLengthValid = credentialDraft.length >= 32 && credentialDraft.length <= 512; + + const handleConfigureCredential = () => { + const configured = configureCredential(credentialDraft); + setCredentialError(!configured); + if (configured) setCredentialDraft(''); + }; + + return ( + + + + + RuView NLOS + + Hidden target hypotheses from a RuView reconstruction server + + + LABS + + + + + This client does not access raw iPhone LiDAR timing data. Safari and Expo display authenticated RuView track frames or visibly watermarked synthetic replay only. + + + + + + + + + {isSynthetic && ( + + SYNTHETIC + + )} + {freshness === 'stale' && ( + + STALE FRAME + + )} + + + + + {visibleTracks.length} + TRACKS + + + + {visibleTracks.length ? `${Math.round(visibleTracks.reduce((sum, track) => sum + track.confidence, 0) / visibleTracks.length * 100)}%` : 'N/A'} + + MEAN CONFIDENCE + + + + + + USE SYNTHETIC REPLAY + + + CONNECT AUTHENTICATED LIVE + + + + {!liveCredentialAvailable ? ( + + EPHEMERAL LIVE CREDENTIAL + { + setCredentialDraft(value); + setCredentialError(false); + }} + secureTextEntry + autoCapitalize="none" + autoCorrect={false} + autoComplete="off" + textContentType="oneTimeCode" + maxLength={512} + placeholder="32 to 512 character pairing credential" + placeholderTextColor={colors.textSecondary} + style={[styles.credentialInput, credentialError && styles.credentialInputError]} + /> + + UNLOCK AUTHENTICATED LIVE + + + A native host or signed in web session may supply this credential automatically. It is held in memory only, sent solely in the ticket request Authorization header, and never stored by this client. + + + ) : ( + + EPHEMERAL CREDENTIAL READY + + FORGET + + + )} + {lastRejectedReason && ( + + Rejected {rejectedFrameCount} frame{rejectedFrameCount === 1 ? '' : 's'}; latest reason: {lastRejectedReason} + + )} + + + ); +}; + +export default NLOSScreen; + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bg }, + content: { padding: spacing.md, paddingBottom: spacing.xxxl, gap: spacing.md }, + header: { flexDirection: 'row', alignItems: 'center', gap: spacing.md }, + notice: { + backgroundColor: 'rgba(255, 165, 2, 0.08)', + borderColor: 'rgba(255, 165, 2, 0.4)', + borderWidth: 1, + borderRadius: 10, + padding: spacing.md, + }, + visualizationCard: { + position: 'relative', + overflow: 'hidden', + backgroundColor: colors.surface, + borderColor: colors.border, + borderWidth: 1, + borderRadius: 12, + paddingTop: spacing.sm, + }, + picker: { flexDirection: 'row', paddingHorizontal: spacing.sm, gap: spacing.sm }, + pickerButton: { flex: 1, alignItems: 'center', paddingVertical: spacing.sm, borderBottomWidth: 2, borderBottomColor: colors.border }, + pickerButtonSelected: { borderBottomColor: colors.accent }, + watermark: { ...StyleSheet.absoluteFill, alignItems: 'center', justifyContent: 'center', transform: [{ rotate: '-18deg' }] }, + watermarkText: { color: 'rgba(255, 165, 2, 0.18)', letterSpacing: 5 }, + staleOverlay: { ...StyleSheet.absoluteFill, backgroundColor: 'rgba(10, 14, 26, 0.7)', alignItems: 'center', justifyContent: 'center' }, + summaryRow: { flexDirection: 'row', gap: spacing.md }, + metric: { flex: 1, backgroundColor: colors.surface, borderRadius: 10, padding: spacing.md }, + actions: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm }, + secondaryButton: { flexGrow: 1, alignItems: 'center', borderColor: colors.border, borderWidth: 1, borderRadius: 8, padding: spacing.md }, + liveButton: { flexGrow: 1, alignItems: 'center', backgroundColor: colors.accent, borderRadius: 8, padding: spacing.md }, + disabledButton: { opacity: 0.35 }, + credentialCard: { backgroundColor: colors.surface, borderColor: colors.border, borderWidth: 1, borderRadius: 10, padding: spacing.md, gap: spacing.sm }, + credentialInput: { borderColor: colors.border, borderWidth: 1, borderRadius: 8, padding: spacing.md, color: colors.textPrimary, backgroundColor: colors.bg }, + credentialInputError: { borderColor: colors.danger }, + credentialButton: { alignItems: 'center', borderColor: colors.accent, borderWidth: 1, borderRadius: 8, padding: spacing.md }, + credentialReadyRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: colors.surface, borderRadius: 10, padding: spacing.md }, +}); diff --git a/ui/mobile/src/screens/SettingsScreen/NlosServerUrlInput.tsx b/ui/mobile/src/screens/SettingsScreen/NlosServerUrlInput.tsx new file mode 100644 index 00000000..59d6bfbf --- /dev/null +++ b/ui/mobile/src/screens/SettingsScreen/NlosServerUrlInput.tsx @@ -0,0 +1,65 @@ +import { Pressable, TextInput, View } from 'react-native'; +import { ThemedText } from '@/components/ThemedText'; +import { colors } from '@/theme/colors'; +import { spacing } from '@/theme/spacing'; +import { normalizeNlosServerUrl } from '@/utils/nlosServerUrl'; + +interface NlosServerUrlInputProps { + value: string; + onChange: (value: string) => void; + onSave: () => void; +} + +export const NlosServerUrlInput = ({ value, onChange, onSave }: NlosServerUrlInputProps) => { + const validation = normalizeNlosServerUrl(value); + + return ( + + + RuView NLOS server URL + + + {!validation.valid && ( + + {validation.error} + + )} + + Separate from the CSI endpoint. Live access requires an ephemeral Bearer credential. + + + + Save NLOS server + + + + ); +}; diff --git a/ui/mobile/src/screens/SettingsScreen/index.tsx b/ui/mobile/src/screens/SettingsScreen/index.tsx index c6eedf5e..10ea68ea 100644 --- a/ui/mobile/src/screens/SettingsScreen/index.tsx +++ b/ui/mobile/src/screens/SettingsScreen/index.tsx @@ -12,6 +12,7 @@ import { Alert, Pressable, Platform } from 'react-native'; import { ThemePicker } from './ThemePicker'; import { RssiToggle } from './RssiToggle'; import { ServerUrlInput } from './ServerUrlInput'; +import { NlosServerUrlInput } from './NlosServerUrlInput'; type GlowCardProps = { title: string; @@ -82,19 +83,26 @@ const ScanIntervalPicker = ({ export const SettingsScreen = () => { const serverUrl = useSettingsStore((state) => state.serverUrl); + const nlosServerUrl = useSettingsStore((state) => state.nlosServerUrl); const rssiScanEnabled = useSettingsStore((state) => state.rssiScanEnabled); const theme = useSettingsStore((state) => state.theme); const setServerUrl = useSettingsStore((state) => state.setServerUrl); + const setNlosServerUrl = useSettingsStore((state) => state.setNlosServerUrl); const setRssiScanEnabled = useSettingsStore((state) => state.setRssiScanEnabled); const setTheme = useSettingsStore((state) => state.setTheme); const [draftUrl, setDraftUrl] = useState(serverUrl); + const [draftNlosUrl, setDraftNlosUrl] = useState(nlosServerUrl); const [scanInterval, setScanInterval] = useState(2); useEffect(() => { setDraftUrl(serverUrl); }, [serverUrl]); + useEffect(() => { + setDraftNlosUrl(nlosServerUrl); + }, [nlosServerUrl]); + const intervalSummary = useMemo(() => `${scanInterval}s`, [scanInterval]); const handleSaveUrl = () => { @@ -105,6 +113,10 @@ export const SettingsScreen = () => { apiService.setBaseUrl(newUrl); }; + const handleSaveNlosUrl = () => { + setNlosServerUrl(draftNlosUrl.trim()); + }; + const handleOpenGitHub = async () => { const handled = await Linking.canOpenURL('https://github.com'); if (!handled) { @@ -126,6 +138,14 @@ export const SettingsScreen = () => { + + + + diff --git a/ui/mobile/src/services/nlos.service.ts b/ui/mobile/src/services/nlos.service.ts new file mode 100644 index 00000000..a4bf68d2 --- /dev/null +++ b/ui/mobile/src/services/nlos.service.ts @@ -0,0 +1,562 @@ +import { + NLOS_MAX_MESSAGE_BYTES, + NLOS_TRACK_SCHEMA, + type NlosFrameEvent, + type NlosRejectReason, + type NlosStreamStatus, + type NlosTrackFrame, +} from '@/types/nlos'; +import { parseNlosTrackFrame, utf8ByteLength } from './nlos.validation'; +import { normalizeNlosServerUrl } from '@/utils/nlosServerUrl'; + +export const NLOS_WS_TICKET_PATH = '/api/v1/nlos/ws-ticket'; +export const NLOS_TICKET_SCHEMA = 'ruview.nlos.ws-ticket.v1' as const; +export const NLOS_AUTHENTICATED_SCHEMA = 'ruview.nlos.authenticated.v1' as const; + +const MAX_TICKET_RESPONSE_BYTES = 8 * 1024; +const MAX_TICKET_TTL_MS = 30_000; +const MAX_AUTHENTICATED_SESSION_TTL_MS = 60 * 60 * 1_000; +const MIN_BEARER_TOKEN_LENGTH = 32; +const MAX_BEARER_TOKEN_LENGTH = 512; +const MAX_CLOCK_SKEW_MS = 1_000; +const TRANSPORT_HANDSHAKE_TIMEOUT_MS = 5_000; +const MAX_REPLAY_FPS = 30; +const SYNTHETIC_SESSION_ID = 'synthetic-replay-v1'; +const ZERO_CALIBRATION_HASH = '0'.repeat(64); + +type FrameListener = (event: NlosFrameEvent) => void; +type StatusListener = (status: NlosStreamStatus) => void; +type RejectListener = (reason: NlosRejectReason) => void; + +interface TicketResponse { + schema: typeof NLOS_TICKET_SCHEMA; + webSocketUrl: string; + expiresAtUnixMs: number; +} + +interface AuthenticatedMessage { + schema: typeof NLOS_AUTHENTICATED_SCHEMA; + sessionId: string; + expiresAtUnixMs: number; +} + +interface FetchResponseLike { + ok: boolean; + status: number; + text: () => Promise; +} + +type FetchLike = (input: string, init: RequestInit) => Promise; + +interface WebSocketLike { + readyState: number; + onopen: (() => void) | null; + onmessage: ((event: { data: unknown }) => void) | null; + onerror: (() => void) | null; + onclose: ((event: { code: number }) => void) | null; + close: (code?: number, reason?: string) => void; +} + +export interface NlosServiceDependencies { + fetch: FetchLike; + createWebSocket: (url: string) => WebSocketLike; + now: () => number; + setInterval: typeof globalThis.setInterval; + clearInterval: typeof globalThis.clearInterval; + setTimeout: typeof globalThis.setTimeout; + clearTimeout: typeof globalThis.clearTimeout; +} + +export interface NlosLiveConfig { + serverUrl: string; + bearerToken: string; +} + +let ephemeralBearerToken: string | null = null; + +const isValidBearerToken = (value: string): boolean => + value.length >= MIN_BEARER_TOKEN_LENGTH && + value.length <= MAX_BEARER_TOKEN_LENGTH && + /^[!-~]+$/.test(value); + +/** Stores an NLOS credential in module memory only. It is never persisted. */ +export const configureNlosBearerToken = (token: string | null): boolean => { + if (token === null) { + ephemeralBearerToken = null; + return true; + } + if (!isValidBearerToken(token)) return false; + ephemeralBearerToken = token; + return true; +}; + +export const hasConfiguredNlosBearerToken = (): boolean => ephemeralBearerToken !== null; + +const consumeConfiguredNlosBearerToken = (): string | null => ephemeralBearerToken; + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const hasExactKeys = (value: Record, keys: readonly string[]): boolean => { + const actual = Object.keys(value); + return actual.length === keys.length && actual.every((key) => keys.includes(key)); +}; + +const isSafeId = (value: unknown): value is string => + typeof value === 'string' && + value.length >= 1 && + value.length <= 64 && + /^[A-Za-z0-9._:-]+$/.test(value); + +const isSafeUnixMs = (value: unknown): value is number => + typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; + +const parseServerUrl = (raw: string): URL | null => { + const validation = normalizeNlosServerUrl(raw); + return validation.valid && validation.normalized ? new URL(validation.normalized) : null; +}; + +const effectivePort = (url: URL): string => { + if (url.port) return url.port; + return url.protocol === 'https:' || url.protocol === 'wss:' ? '443' : '80'; +}; + +const parseWebSocketUrl = (raw: string, serverUrl: URL): string | null => { + try { + const url = new URL(raw); + const secure = url.protocol === 'wss:'; + const loopback = url.protocol === 'ws:' && + (url.hostname === 'localhost' || + url.hostname === '127.0.0.1' || + url.hostname === '[::1]' || + url.hostname === '::1'); + const expectedProtocol = serverUrl.protocol === 'https:' ? 'wss:' : 'ws:'; + const ticketKeys = Array.from(url.searchParams.keys()); + const ticket = url.searchParams.get('ticket'); + if ( + (!secure && !loopback) || + url.protocol !== expectedProtocol || + url.hostname !== serverUrl.hostname || + effectivePort(url) !== effectivePort(serverUrl) || + url.pathname !== '/api/v1/nlos/ws' || + ticketKeys.length !== 1 || + ticketKeys[0] !== 'ticket' || + !ticket || + !/^[0-9a-f]{64}$/.test(ticket) || + url.username || + url.password || + url.hash + ) return null; + return url.toString(); + } catch { + return null; + } +}; + +const parseTicket = (raw: string, now: number, serverUrl: URL): TicketResponse | null => { + if (utf8ByteLength(raw) > MAX_TICKET_RESPONSE_BYTES) return null; + try { + const value = JSON.parse(raw) as unknown; + if ( + !isRecord(value) || + !hasExactKeys(value, ['schema', 'webSocketUrl', 'expiresAtUnixMs']) || + value.schema !== NLOS_TICKET_SCHEMA || + typeof value.webSocketUrl !== 'string' || + parseWebSocketUrl(value.webSocketUrl, serverUrl) === null || + !isSafeUnixMs(value.expiresAtUnixMs) || + value.expiresAtUnixMs <= now || + value.expiresAtUnixMs - now > MAX_TICKET_TTL_MS + ) { + return null; + } + return value as unknown as TicketResponse; + } catch { + return null; + } +}; + +const parseAuthenticatedMessage = (raw: string, now: number): AuthenticatedMessage | null => { + if (utf8ByteLength(raw) > NLOS_MAX_MESSAGE_BYTES) return null; + try { + const value = JSON.parse(raw) as unknown; + if ( + !isRecord(value) || + !hasExactKeys(value, ['schema', 'sessionId', 'expiresAtUnixMs']) || + value.schema !== NLOS_AUTHENTICATED_SCHEMA || + !isSafeId(value.sessionId) || + !isSafeUnixMs(value.expiresAtUnixMs) || + value.expiresAtUnixMs <= now || + value.expiresAtUnixMs - now > MAX_AUTHENTICATED_SESSION_TTL_MS + MAX_CLOCK_SKEW_MS + ) { + return null; + } + return value as unknown as AuthenticatedMessage; + } catch { + return null; + } +}; + +export const createSyntheticNlosFrame = ( + sequence: number, + now: number, + fps = 15, +): NlosTrackFrame => { + if ( + !Number.isSafeInteger(sequence) || + sequence < 0 || + !Number.isSafeInteger(now) || + now < 0 || + now > Number.MAX_SAFE_INTEGER - 1_000 || + !Number.isFinite(fps) || + fps < 1 || + fps > MAX_REPLAY_FPS + ) { + throw new RangeError('Synthetic sequence and timestamp must be safe unsigned integers'); + } + const phase = sequence / 12; + const phaseRate = fps / 12; + const x = 2.4 + Math.sin(phase) * 1.3; + const y = 1.05 + Math.sin(phase * 0.4) * 0.08; + const zPhase = phase * 0.7; + const z = 3.3 + Math.cos(zPhase) * 0.9; + const vx = Math.cos(phase) * 1.3 * phaseRate; + const vy = Math.cos(phase * 0.4) * 0.08 * 0.4 * phaseRate; + const vz = -Math.sin(zPhase) * 0.9 * 0.7 * phaseRate; + + return { + schema: NLOS_TRACK_SCHEMA, + sessionId: SYNTHETIC_SESSION_ID, + sequence, + capturedAtUnixMs: now, + expiresAtUnixMs: now + 1_000, + source: 'synthetic', + evidenceLevel: 'l0_synthetic', + algorithmVersion: 'synthetic-replay-v1', + calibrationHash: ZERO_CALIBRATION_HASH, + provenance: { + sensorId: 'synthetic-sensor', + sensorModel: 'deterministic-fixture', + firmwareVersion: 'fixture-v1', + transientKind: 'replay', + histogramPreserved: false, + transport: 'replay', + }, + tracks: [ + { + trackId: 'synthetic-target-1', + state: 'tracking', + positionM: { x, y, z }, + velocityMps: { x: vx, y: vy, z: vz }, + covarianceDiagonalM2: { x: 0.12, y: 0.18, z: 0.14 }, + confidence: 0.72, + posteriorEntropy: 0.68, + signalQuality: 0.64, + modalityContributions: { lidar: 0.55, csi: 0.45 }, + }, + ], + }; +}; + +const defaultDependencies = (): NlosServiceDependencies => ({ + fetch: (input, init) => fetch(input, init) as Promise, + createWebSocket: (url) => new WebSocket(url) as unknown as WebSocketLike, + now: Date.now, + setInterval: globalThis.setInterval.bind(globalThis), + clearInterval: globalThis.clearInterval.bind(globalThis), + setTimeout: globalThis.setTimeout.bind(globalThis), + clearTimeout: globalThis.clearTimeout.bind(globalThis), +}); + +export class NlosService { + private readonly dependencies: NlosServiceDependencies; + private frameListeners = new Set(); + private statusListeners = new Set(); + private rejectListeners = new Set(); + private socket: WebSocketLike | null = null; + private replayTimer: ReturnType | null = null; + private authenticationTimer: ReturnType | null = null; + private sessionExpiryTimer: ReturnType | null = null; + private ticketAbortController: AbortController | null = null; + private status: NlosStreamStatus = 'idle'; + private generation = 0; + private authenticatedSession: AuthenticatedMessage | null = null; + private lastSequence = -1; + private replaySequence = 0; + + constructor(dependencies: NlosServiceDependencies = defaultDependencies()) { + this.dependencies = dependencies; + } + + subscribe(listener: FrameListener): () => void { + this.frameListeners.add(listener); + return () => this.frameListeners.delete(listener); + } + + subscribeStatus(listener: StatusListener): () => void { + this.statusListeners.add(listener); + return () => this.statusListeners.delete(listener); + } + + subscribeRejected(listener: RejectListener): () => void { + this.rejectListeners.add(listener); + return () => this.rejectListeners.delete(listener); + } + + getStatus(): NlosStreamStatus { + return this.status; + } + + async connectLive(config: NlosLiveConfig): Promise { + this.stopTransport(); + const generation = this.generation; + const serverUrl = parseServerUrl(config.serverUrl); + if (!serverUrl || !isValidBearerToken(config.bearerToken)) { + this.setStatus('error'); + this.emitRejected('unauthenticated'); + return false; + } + + this.setStatus('authenticating'); + const ticketUrl = new URL(NLOS_WS_TICKET_PATH, serverUrl).toString(); + const abortController = new AbortController(); + this.ticketAbortController = abortController; + const ticketTimer = this.dependencies.setTimeout( + () => abortController.abort(), + TRANSPORT_HANDSHAKE_TIMEOUT_MS, + ); + + try { + const response = await this.dependencies.fetch(ticketUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${config.bearerToken}`, + }, + body: '', + credentials: 'omit', + redirect: 'error', + signal: abortController.signal, + }); + if (generation !== this.generation) return false; + if (!response.ok) { + this.setStatus('error'); + this.emitRejected('unauthenticated'); + return false; + } + + const ticket = parseTicket(await response.text(), this.dependencies.now(), serverUrl); + if (generation !== this.generation) return false; + if (!ticket) { + this.setStatus('error'); + this.emitRejected('invalid_shape'); + return false; + } + + this.openSocket(ticket.webSocketUrl, generation); + return true; + } catch { + if (generation === this.generation) { + this.setStatus('error'); + this.emitRejected('unauthenticated'); + } + return false; + } finally { + this.dependencies.clearTimeout(ticketTimer); + if (this.ticketAbortController === abortController) this.ticketAbortController = null; + } + } + + connectConfiguredLive(serverUrl: string): Promise { + const token = consumeConfiguredNlosBearerToken(); + if (!token) { + this.setStatus('error'); + this.emitRejected('unauthenticated'); + return Promise.resolve(false); + } + return this.connectLive({ serverUrl, bearerToken: token }); + } + + startDeterministicReplay(fps = 15): void { + this.stopTransport(); + const requestedFps = Number.isFinite(fps) ? Math.floor(fps) : 15; + const boundedFps = Math.max(1, Math.min(MAX_REPLAY_FPS, requestedFps)); + const emitFrame = () => { + const receivedAtUnixMs = this.dependencies.now(); + const frame = createSyntheticNlosFrame(this.replaySequence, receivedAtUnixMs, boundedFps); + this.replaySequence += 1; + this.emitFrame({ frame, channel: 'deterministic_replay', receivedAtUnixMs }); + }; + + this.setStatus('synthetic_replay'); + emitFrame(); + this.replayTimer = this.dependencies.setInterval(emitFrame, Math.ceil(1_000 / boundedFps)); + } + + disconnect(): void { + this.stopTransport(); + this.setStatus('idle'); + } + + private openSocket(url: string, generation: number): void { + this.setStatus('connecting'); + const socket = this.dependencies.createWebSocket(url); + this.socket = socket; + this.authenticationTimer = this.dependencies.setTimeout(() => { + if (generation !== this.generation || this.authenticatedSession) return; + this.authenticationTimer = null; + this.emitRejected('unauthenticated'); + this.setStatus('error'); + socket.close(1008, 'authentication timeout'); + }, TRANSPORT_HANDSHAKE_TIMEOUT_MS); + + socket.onopen = () => { + if (generation === this.generation) this.setStatus('connecting'); + }; + socket.onmessage = (event) => { + if (generation !== this.generation || socket !== this.socket) return; + this.handleSocketMessage(event.data); + }; + socket.onerror = () => { + if (generation === this.generation) this.setStatus('error'); + }; + socket.onclose = (event) => { + if (generation !== this.generation) return; + this.socket = null; + this.authenticatedSession = null; + this.clearAuthenticationTimer(); + this.clearSessionExpiryTimer(); + if (event.code !== 1000) this.setStatus('error'); + else this.setStatus('idle'); + }; + } + + private handleSocketMessage(data: unknown): void { + if (typeof data !== 'string') { + this.emitRejected('unsupported_binary'); + return; + } + + const now = this.dependencies.now(); + if (!this.authenticatedSession) { + const authenticated = parseAuthenticatedMessage(data, now); + if (!authenticated) { + this.emitRejected('unauthenticated'); + this.clearAuthenticationTimer(); + this.setStatus('error'); + this.socket?.close(1008, 'authentication required'); + return; + } + this.authenticatedSession = authenticated; + this.lastSequence = -1; + this.clearAuthenticationTimer(); + this.scheduleSessionExpiry(authenticated, now); + this.setStatus('live'); + return; + } + + if (this.authenticatedSession.expiresAtUnixMs <= now) { + this.emitRejected('unauthenticated'); + this.socket?.close(1008, 'session expired'); + return; + } + + const parsed = parseNlosTrackFrame(data); + if (!parsed.ok) { + this.emitRejected(parsed.reason); + return; + } + const frame = parsed.value; + const authenticatedLive = + frame.source === 'live' && + frame.provenance.transport === 'ruview_server' && + frame.provenance.histogramPreserved; + const authenticatedSynthetic = + frame.source === 'synthetic' && frame.provenance.transport === 'replay'; + if (!authenticatedLive && !authenticatedSynthetic) { + this.emitRejected('invalid_provenance'); + return; + } + if (frame.sessionId !== this.authenticatedSession.sessionId) { + this.emitRejected('session_mismatch'); + return; + } + if (frame.sequence <= this.lastSequence) { + this.emitRejected('out_of_order'); + return; + } + if (frame.capturedAtUnixMs > now + MAX_CLOCK_SKEW_MS) { + this.emitRejected('future_frame'); + return; + } + if (frame.expiresAtUnixMs <= now) { + this.emitRejected('expired'); + return; + } + + this.lastSequence = frame.sequence; + this.emitFrame({ frame, channel: 'authenticated_stream', receivedAtUnixMs: now }); + } + + private stopTransport(): void { + this.generation += 1; + this.authenticatedSession = null; + this.lastSequence = -1; + this.replaySequence = 0; + this.ticketAbortController?.abort(); + this.ticketAbortController = null; + this.clearAuthenticationTimer(); + this.clearSessionExpiryTimer(); + if (this.replayTimer !== null) { + this.dependencies.clearInterval(this.replayTimer); + this.replayTimer = null; + } + if (this.socket) { + const socket = this.socket; + this.socket = null; + socket.close(1000, 'client disconnect'); + } + } + + private setStatus(status: NlosStreamStatus): void { + if (status === this.status) return; + this.status = status; + this.statusListeners.forEach((listener) => listener(status)); + } + + private clearAuthenticationTimer(): void { + if (this.authenticationTimer !== null) { + this.dependencies.clearTimeout(this.authenticationTimer); + this.authenticationTimer = null; + } + } + + private scheduleSessionExpiry(session: AuthenticatedMessage, now: number): void { + this.clearSessionExpiryTimer(); + const delay = Math.max(0, session.expiresAtUnixMs - now); + this.sessionExpiryTimer = this.dependencies.setTimeout(() => { + if (this.authenticatedSession !== session) return; + this.sessionExpiryTimer = null; + this.authenticatedSession = null; + this.emitRejected('unauthenticated'); + this.setStatus('error'); + this.socket?.close(1008, 'session expired'); + }, delay); + } + + private clearSessionExpiryTimer(): void { + if (this.sessionExpiryTimer !== null) { + this.dependencies.clearTimeout(this.sessionExpiryTimer); + this.sessionExpiryTimer = null; + } + } + + private emitFrame(event: NlosFrameEvent): void { + this.frameListeners.forEach((listener) => listener(event)); + } + + private emitRejected(reason: NlosRejectReason): void { + this.rejectListeners.forEach((listener) => listener(reason)); + } +} + +export const nlosService = new NlosService(); diff --git a/ui/mobile/src/services/nlos.validation.ts b/ui/mobile/src/services/nlos.validation.ts new file mode 100644 index 00000000..f9515c7f --- /dev/null +++ b/ui/mobile/src/services/nlos.validation.ts @@ -0,0 +1,248 @@ +import { + NLOS_MAX_EXPIRY_WINDOW_MS, + NLOS_MAX_MESSAGE_BYTES, + NLOS_MAX_TRACKS, + NLOS_TRACK_SCHEMA, + type NlosEvidenceLevel, + type NlosProvenance, + type NlosRejectReason, + type NlosTrack, + type NlosTrackFrame, + type NlosValidationResult, + type NlosVector3, +} from '@/types/nlos'; + +const SAFE_ID = /^[A-Za-z0-9._:-]+$/; +const CALIBRATION_HASH = /^[0-9a-f]{64}$/; +const ZERO_CALIBRATION_HASH = '0'.repeat(64); +const EVIDENCE_LEVELS: ReadonlyArray = [ + 'l0_synthetic', + 'l1_measured', + 'l2_calibrated', + 'l3_corroborated', +]; + +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const hasExactKeys = (value: Record, keys: readonly string[]): boolean => { + const actual = Object.keys(value); + return actual.length === keys.length && actual.every((key) => keys.includes(key)); +}; + +const isSafeId = (value: unknown, maxLength = 64): value is string => + typeof value === 'string' && value.length >= 1 && value.length <= maxLength && SAFE_ID.test(value); + +const isSafeUint = (value: unknown): value is number => + typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; + +const isFiniteInRange = (value: unknown, min: number, max: number): value is number => + typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max; + +const isVector = (value: unknown, absoluteBound: number, nonNegative = false): value is NlosVector3 => { + if (!isRecord(value) || !hasExactKeys(value, ['x', 'y', 'z'])) return false; + const min = nonNegative ? 0 : -absoluteBound; + return ( + isFiniteInRange(value.x, min, absoluteBound) && + isFiniteInRange(value.y, min, absoluteBound) && + isFiniteInRange(value.z, min, absoluteBound) + ); +}; + +const isProvenance = (value: unknown): value is NlosProvenance => { + if ( + !isRecord(value) || + !hasExactKeys(value, [ + 'sensorId', + 'sensorModel', + 'firmwareVersion', + 'transientKind', + 'histogramPreserved', + 'transport', + ]) + ) { + return false; + } + + return ( + isSafeId(value.sensorId, 64) && + isSafeId(value.sensorModel, 64) && + isSafeId(value.firmwareVersion, 64) && + (value.transientKind === 'raw_histogram' || + value.transientKind === 'compact_normalized_histogram' || + value.transientKind === 'depth_only' || + value.transientKind === 'replay') && + typeof value.histogramPreserved === 'boolean' && + (value.transport === 'usb_serial' || value.transport === 'ruview_server' || value.transport === 'replay') + ); +}; + +const isTrack = (value: unknown): value is NlosTrack => { + if ( + !isRecord(value) || + !hasExactKeys(value, [ + 'trackId', + 'state', + 'positionM', + 'velocityMps', + 'covarianceDiagonalM2', + 'confidence', + 'posteriorEntropy', + 'signalQuality', + 'modalityContributions', + ]) + ) { + return false; + } + + if (!isRecord(value.modalityContributions) || !hasExactKeys(value.modalityContributions, ['lidar', 'csi'])) { + return false; + } + + const lidar = value.modalityContributions.lidar; + const csi = value.modalityContributions.csi; + return ( + isSafeId(value.trackId) && + (value.state === 'tracking' || value.state === 'degraded' || value.state === 'unknown') && + isVector(value.positionM, 100) && + isVector(value.velocityMps, 20) && + isVector(value.covarianceDiagonalM2, 10, true) && + isFiniteInRange(value.confidence, 0, 1) && + typeof value.posteriorEntropy === 'number' && + Number.isFinite(value.posteriorEntropy) && + value.posteriorEntropy >= 0 && + isFiniteInRange(value.signalQuality, 0, 1) && + isFiniteInRange(lidar, 0, 1) && + isFiniteInRange(csi, 0, 1) && + lidar + csi >= 0.999 && + lidar + csi <= 1.001 + ); +}; + +const classifyShapeFailure = (value: Record): NlosRejectReason => { + if (value.schema !== NLOS_TRACK_SCHEMA) return 'invalid_schema'; + if ( + !isSafeUint(value.sequence) || + !isSafeUint(value.capturedAtUnixMs) || + !isSafeUint(value.expiresAtUnixMs) || + !Array.isArray(value.tracks) || + value.tracks.length > NLOS_MAX_TRACKS + ) { + return 'invalid_bounds'; + } + return 'invalid_shape'; +}; + +export const utf8ByteLength = (value: string): number => { + let bytes = 0; + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x80) bytes += 1; + else if (code < 0x800) bytes += 2; + else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) { + const next = value.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + index += 1; + } else { + bytes += 3; + } + } else bytes += 3; + } + return bytes; +}; + +export const validateNlosTrackFrame = (value: unknown): NlosValidationResult => { + if (!isRecord(value)) return { ok: false, reason: 'invalid_shape' }; + if ( + !hasExactKeys(value, [ + 'schema', + 'sessionId', + 'sequence', + 'capturedAtUnixMs', + 'expiresAtUnixMs', + 'source', + 'evidenceLevel', + 'algorithmVersion', + 'calibrationHash', + 'provenance', + 'tracks', + ]) || + value.schema !== NLOS_TRACK_SCHEMA || + !isSafeId(value.sessionId) || + !isSafeUint(value.sequence) || + !isSafeUint(value.capturedAtUnixMs) || + !isSafeUint(value.expiresAtUnixMs) || + (value.source !== 'live' && value.source !== 'replay' && value.source !== 'synthetic') || + !EVIDENCE_LEVELS.includes(value.evidenceLevel as NlosEvidenceLevel) || + !isSafeId(value.algorithmVersion) || + typeof value.calibrationHash !== 'string' || + !CALIBRATION_HASH.test(value.calibrationHash) || + !isProvenance(value.provenance) || + !Array.isArray(value.tracks) || + value.tracks.length > NLOS_MAX_TRACKS || + !value.tracks.every(isTrack) + ) { + return { ok: false, reason: classifyShapeFailure(value) }; + } + + const frame = value as unknown as NlosTrackFrame; + if (frame.evidenceLevel === 'l3_corroborated') { + return { ok: false, reason: 'invalid_provenance' }; + } + const expiryWindow = frame.expiresAtUnixMs - frame.capturedAtUnixMs; + if (expiryWindow <= 0 || expiryWindow > NLOS_MAX_EXPIRY_WINDOW_MS) { + return { ok: false, reason: 'invalid_bounds' }; + } + + const evidenceIndex = EVIDENCE_LEVELS.indexOf(frame.evidenceLevel); + const liveProvenanceValid = + frame.source !== 'live' || + (evidenceIndex >= EVIDENCE_LEVELS.indexOf('l1_measured') && + frame.provenance.histogramPreserved && + frame.provenance.transientKind !== 'depth_only' && + frame.provenance.transientKind !== 'replay' && + frame.provenance.transport !== 'replay'); + const syntheticProvenanceValid = + frame.source !== 'synthetic' || + (frame.evidenceLevel === 'l0_synthetic' && + frame.calibrationHash === ZERO_CALIBRATION_HASH && + frame.provenance.transport === 'replay' && + frame.provenance.transientKind === 'replay'); + const replayProvenanceValid = + frame.source !== 'replay' || + (frame.provenance.transport === 'replay' && + frame.provenance.transientKind === 'replay' && + frame.provenance.histogramPreserved); + const depthOnlyNotLive = frame.provenance.transientKind !== 'depth_only' || frame.source !== 'live'; + const calibratedHashValid = + evidenceIndex < EVIDENCE_LEVELS.indexOf('l2_calibrated') || + frame.calibrationHash !== ZERO_CALIBRATION_HASH; + const trackIds = new Set(frame.tracks.map((track) => track.trackId)); + const trackIdsUnique = trackIds.size === frame.tracks.length; + + if ( + !liveProvenanceValid || + !syntheticProvenanceValid || + !replayProvenanceValid || + !depthOnlyNotLive || + !calibratedHashValid || + !trackIdsUnique + ) { + return { ok: false, reason: 'invalid_provenance' }; + } + + return { ok: true, value: frame }; +}; + +export const parseNlosTrackFrame = (raw: string): NlosValidationResult => { + if (utf8ByteLength(raw) > NLOS_MAX_MESSAGE_BYTES) { + return { ok: false, reason: 'message_too_large' }; + } + + try { + return validateNlosTrackFrame(JSON.parse(raw) as unknown); + } catch { + return { ok: false, reason: 'malformed_json' }; + } +}; diff --git a/ui/mobile/src/stores/nlosStore.ts b/ui/mobile/src/stores/nlosStore.ts new file mode 100644 index 00000000..70244d10 --- /dev/null +++ b/ui/mobile/src/stores/nlosStore.ts @@ -0,0 +1,137 @@ +import { create } from 'zustand'; +import { validateNlosTrackFrame } from '@/services/nlos.validation'; +import { + NLOS_STALE_AFTER_MS, + type NlosFrameEvent, + type NlosFreshness, + type NlosRejectReason, + type NlosStreamStatus, + type NlosTrackFrame, +} from '@/types/nlos'; + +export interface NlosState { + streamStatus: NlosStreamStatus; + freshness: NlosFreshness; + frame: NlosTrackFrame | null; + lastReceivedAtUnixMs: number | null; + lastRejectedReason: NlosRejectReason | null; + rejectedFrameCount: number; + ingestFrame: (event: NlosFrameEvent) => void; + setStreamStatus: (status: NlosStreamStatus) => void; + recordRejection: (reason: NlosRejectReason) => void; + refreshFreshness: (now: number) => void; + reset: () => void; +} + +const initialState = { + streamStatus: 'idle' as NlosStreamStatus, + freshness: 'unknown' as NlosFreshness, + frame: null as NlosTrackFrame | null, + lastReceivedAtUnixMs: null as number | null, + lastRejectedReason: null as NlosRejectReason | null, + rejectedFrameCount: 0, +}; + +export const useNlosStore = create((set) => ({ + ...initialState, + + ingestFrame: (event) => { + const validation = validateNlosTrackFrame(event.frame); + if (!validation.ok) { + set((state) => ({ + lastRejectedReason: validation.reason, + rejectedFrameCount: state.rejectedFrameCount + 1, + })); + return; + } + + const frame = validation.value; + const channelValid = + (event.channel === 'authenticated_stream' && + (frame.source === 'live' || frame.source === 'synthetic')) || + (event.channel === 'deterministic_replay' && frame.source === 'synthetic'); + if (!channelValid) { + set((state) => ({ + lastRejectedReason: frame.source === 'live' ? 'unauthenticated' : 'invalid_provenance', + rejectedFrameCount: state.rejectedFrameCount + 1, + })); + return; + } + + set((state) => { + if ( + state.frame?.sessionId === frame.sessionId && + frame.sequence <= state.frame.sequence + ) { + return { + lastRejectedReason: 'out_of_order', + rejectedFrameCount: state.rejectedFrameCount + 1, + }; + } + if (frame.expiresAtUnixMs <= event.receivedAtUnixMs) { + return { + lastRejectedReason: 'expired', + rejectedFrameCount: state.rejectedFrameCount + 1, + }; + } + + return { + frame, + freshness: 'fresh', + lastReceivedAtUnixMs: event.receivedAtUnixMs, + lastRejectedReason: null, + }; + }); + }, + + setStreamStatus: (streamStatus) => + set((state) => { + if (streamStatus === 'live' || streamStatus === 'synthetic_replay') { + return { streamStatus }; + } + if ( + state.frame === null && + state.freshness === 'unknown' && + state.lastReceivedAtUnixMs === null + ) { + return { streamStatus }; + } + return { + streamStatus, + frame: null, + freshness: 'unknown', + lastReceivedAtUnixMs: null, + }; + }), + + recordRejection: (reason) => + set((state) => ({ + frame: null, + freshness: 'unknown', + lastReceivedAtUnixMs: null, + lastRejectedReason: reason, + rejectedFrameCount: state.rejectedFrameCount + 1, + })), + + refreshFreshness: (now) => + set((state) => { + if (!state.frame || state.lastReceivedAtUnixMs === null) { + return state.freshness === 'unknown' ? {} : { freshness: 'unknown' }; + } + const stale = + now < state.lastReceivedAtUnixMs || + now >= state.frame.expiresAtUnixMs || + now - state.lastReceivedAtUnixMs > NLOS_STALE_AFTER_MS; + const nextFreshness: NlosFreshness = stale ? 'stale' : 'fresh'; + if (stale) { + return { + frame: null, + freshness: nextFreshness, + lastReceivedAtUnixMs: null, + }; + } + return state.freshness === nextFreshness ? {} : { freshness: nextFreshness }; + }), + + reset: () => set(initialState), +})); diff --git a/ui/mobile/src/stores/settingsStore.ts b/ui/mobile/src/stores/settingsStore.ts index f1ec81d5..63ed0d27 100644 --- a/ui/mobile/src/stores/settingsStore.ts +++ b/ui/mobile/src/stores/settingsStore.ts @@ -1,15 +1,18 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; +import { normalizeNlosServerUrl } from '@/utils/nlosServerUrl'; export type Theme = 'light' | 'dark' | 'system'; export interface SettingsState { serverUrl: string; + nlosServerUrl: string; rssiScanEnabled: boolean; theme: Theme; alertSoundEnabled: boolean; setServerUrl: (url: string) => void; + setNlosServerUrl: (url: string) => void; setRssiScanEnabled: (value: boolean) => void; setTheme: (theme: Theme) => void; setAlertSoundEnabled: (value: boolean) => void; @@ -19,6 +22,7 @@ export const useSettingsStore = create()( persist( (set) => ({ serverUrl: 'http://localhost:3000', + nlosServerUrl: 'http://localhost:3000', rssiScanEnabled: false, theme: 'system', alertSoundEnabled: true, @@ -27,6 +31,13 @@ export const useSettingsStore = create()( set({ serverUrl: url }); }, + setNlosServerUrl: (url) => { + const validation = normalizeNlosServerUrl(url); + if (validation.valid && validation.normalized) { + set({ nlosServerUrl: validation.normalized }); + } + }, + setRssiScanEnabled: (value) => { set({ rssiScanEnabled: value }); }, diff --git a/ui/mobile/src/testUtils/nlosFixtures.ts b/ui/mobile/src/testUtils/nlosFixtures.ts new file mode 100644 index 00000000..91f67f84 --- /dev/null +++ b/ui/mobile/src/testUtils/nlosFixtures.ts @@ -0,0 +1,37 @@ +import { NLOS_TRACK_SCHEMA, type NlosTrackFrame } from '@/types/nlos'; + +export const createLiveNlosFrameFixture = ( + overrides: Partial = {}, +): NlosTrackFrame => ({ + schema: NLOS_TRACK_SCHEMA, + sessionId: 'live-session-1', + sequence: 1, + capturedAtUnixMs: 1_700_000_000_000, + expiresAtUnixMs: 1_700_000_001_000, + source: 'live', + evidenceLevel: 'l2_calibrated', + algorithmVersion: 'nlos-inversion-v1', + calibrationHash: 'a'.repeat(64), + provenance: { + sensorId: 'tof-1', + sensorModel: 'VL53L8CH', + firmwareVersion: '1.0.0', + transientKind: 'raw_histogram', + histogramPreserved: true, + transport: 'ruview_server', + }, + tracks: [ + { + trackId: 'target-1', + state: 'tracking', + positionM: { x: 2.1, y: 1.1, z: 3.4 }, + velocityMps: { x: 0.1, y: 0, z: -0.05 }, + covarianceDiagonalM2: { x: 0.04, y: 0.06, z: 0.08 }, + confidence: 0.88, + posteriorEntropy: 0.32, + signalQuality: 0.81, + modalityContributions: { lidar: 0.7, csi: 0.3 }, + }, + ], + ...overrides, +}); diff --git a/ui/mobile/src/types/navigation.ts b/ui/mobile/src/types/navigation.ts index 9eb5906b..af1526be 100644 --- a/ui/mobile/src/types/navigation.ts +++ b/ui/mobile/src/types/navigation.ts @@ -4,6 +4,7 @@ export type RootStackParamList = { export type MainTabsParamList = { Live: undefined; + NLOS: undefined; Vitals: undefined; Zones: undefined; MAT: undefined; @@ -11,6 +12,7 @@ export type MainTabsParamList = { }; export type LiveScreenParams = undefined; +export type NLOSScreenParams = undefined; export type VitalsScreenParams = undefined; export type ZonesScreenParams = undefined; export type MATScreenParams = undefined; diff --git a/ui/mobile/src/types/nlos.ts b/ui/mobile/src/types/nlos.ts new file mode 100644 index 00000000..cef0eccb --- /dev/null +++ b/ui/mobile/src/types/nlos.ts @@ -0,0 +1,100 @@ +export const NLOS_TRACK_SCHEMA = 'ruview.nlos.track.v1' as const; +export const NLOS_MAX_MESSAGE_BYTES = 256 * 1024; +export const NLOS_MAX_TRACKS = 16; +export const NLOS_MAX_EXPIRY_WINDOW_MS = 5_000; +export const NLOS_STALE_AFTER_MS = 1_500; + +export type NlosSource = 'live' | 'replay' | 'synthetic'; +export type NlosEvidenceLevel = + | 'l0_synthetic' + | 'l1_measured' + | 'l2_calibrated' + | 'l3_corroborated'; +export type NlosTransientKind = + | 'raw_histogram' + | 'compact_normalized_histogram' + | 'depth_only' + | 'replay'; +export type NlosTransport = 'usb_serial' | 'ruview_server' | 'replay'; +export type NlosTrackState = 'tracking' | 'degraded' | 'unknown'; + +export interface NlosVector3 { + x: number; + y: number; + z: number; +} + +export interface NlosModalityContributions { + lidar: number; + csi: number; +} + +export interface NlosTrack { + trackId: string; + state: NlosTrackState; + positionM: NlosVector3; + velocityMps: NlosVector3; + covarianceDiagonalM2: NlosVector3; + confidence: number; + posteriorEntropy: number; + signalQuality: number; + modalityContributions: NlosModalityContributions; +} + +export interface NlosProvenance { + sensorId: string; + sensorModel: string; + firmwareVersion: string; + transientKind: NlosTransientKind; + histogramPreserved: boolean; + transport: NlosTransport; +} + +export interface NlosTrackFrame { + schema: typeof NLOS_TRACK_SCHEMA; + sessionId: string; + sequence: number; + capturedAtUnixMs: number; + expiresAtUnixMs: number; + source: NlosSource; + evidenceLevel: NlosEvidenceLevel; + algorithmVersion: string; + calibrationHash: string; + provenance: NlosProvenance; + tracks: NlosTrack[]; +} + +export type NlosFreshness = 'unknown' | 'fresh' | 'stale'; +export type NlosStreamStatus = + | 'idle' + | 'authenticating' + | 'connecting' + | 'live' + | 'synthetic_replay' + | 'error'; + +export type NlosFrameChannel = 'authenticated_stream' | 'deterministic_replay'; + +export interface NlosFrameEvent { + frame: NlosTrackFrame; + channel: NlosFrameChannel; + receivedAtUnixMs: number; +} + +export type NlosRejectReason = + | 'message_too_large' + | 'malformed_json' + | 'invalid_schema' + | 'invalid_shape' + | 'invalid_bounds' + | 'invalid_provenance' + | 'expired' + | 'future_frame' + | 'session_mismatch' + | 'out_of_order' + | 'unauthenticated' + | 'unsupported_binary'; + +export type NlosValidationResult = + | { ok: true; value: NlosTrackFrame } + | { ok: false; reason: NlosRejectReason }; diff --git a/ui/mobile/src/utils/nlosServerUrl.ts b/ui/mobile/src/utils/nlosServerUrl.ts new file mode 100644 index 00000000..35507061 --- /dev/null +++ b/ui/mobile/src/utils/nlosServerUrl.ts @@ -0,0 +1,65 @@ +export interface NlosServerUrlValidation { + valid: boolean; + error?: string; + normalized?: string; +} + +// Hermes does not provide TextEncoder in every supported React Native build. +// Count UTF-8 bytes without allocating an encoded copy. +const utf8Length = (value: string): number => { + let bytes = 0; + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code < 0x80) bytes += 1; + else if (code < 0x800) bytes += 2; + else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) { + const next = value.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + bytes += 4; + index += 1; + } else bytes += 3; + } else bytes += 3; + } + return bytes; +}; + +const isLoopback = (hostname: string): boolean => + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '[::1]' || + hostname === '::1'; + +/** Validate and reduce an NLOS endpoint to an origin-only URL before storage. */ +export const normalizeNlosServerUrl = (raw: string): NlosServerUrlValidation => { + const value = raw.trim(); + if (!value || utf8Length(value) > 2_048) { + return { valid: false, error: 'NLOS server URL must be 1 to 2048 bytes.' }; + } + + try { + const url = new URL(value); + const secure = url.protocol === 'https:'; + const loopbackDevelopment = url.protocol === 'http:' && isLoopback(url.hostname); + if (!secure && !loopbackDevelopment) { + return { + valid: false, + error: 'NLOS requires HTTPS, except for a loopback development server.', + }; + } + if ( + url.username || + url.password || + url.search || + url.hash || + (url.pathname !== '' && url.pathname !== '/') + ) { + return { + valid: false, + error: 'Store only the NLOS server origin; credentials, paths, queries, and fragments are forbidden.', + }; + } + return { valid: true, normalized: url.origin }; + } catch { + return { valid: false, error: 'Enter a valid NLOS server origin.' }; + } +}; diff --git a/v2/Cargo.lock b/v2/Cargo.lock index 07bac71d..4b60f391 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -9476,6 +9476,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ruview-nlos" +version = "0.3.1" +dependencies = [ + "axum", + "clap", + "getrandom 0.2.17", + "http-body-util", + "serde", + "serde_json", + "serialport", + "sha2 0.10.9", + "thiserror 2.0.18", + "tokio", + "tower 0.5.3", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ruview-offaxis" version = "0.3.1" diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 629bf0ee..84ce5359 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -123,6 +123,9 @@ members = [ # ADR-324 — clean-room Kooima off-axis (head-coupled perspective) projection. # Dependency-free native core; wasm-bindgen surface only on wasm32. "crates/ruview-offaxis", + # ADR-328..331 — consumer ToF transient NLOS capture, motion-induced + # aperture tracking, governed CSI fusion, and native/web client contract. + "crates/ruview-nlos", ] # ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std), # excluded from workspace to avoid breaking `cargo test --workspace`. diff --git a/v2/crates/ruview-nlos/Cargo.toml b/v2/crates/ruview-nlos/Cargo.toml new file mode 100644 index 00000000..d33b3f48 --- /dev/null +++ b/v2/crates/ruview-nlos/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "ruview-nlos" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Governed consumer ToF transient NLOS tracking and CSI fusion for RuView" + +[features] +default = ["server"] +server = [ + "dep:axum", + "dep:clap", + "dep:getrandom", + "dep:tokio", + "dep:tracing", + "dep:tracing-subscriber", + "dep:tower-http", +] +hardware = ["dep:serialport"] + +[dependencies] +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +thiserror.workspace = true +axum = { workspace = true, optional = true } +clap = { workspace = true, optional = true } +getrandom = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, optional = true } +tower-http = { workspace = true, optional = true, features = ["limit"] } +serialport = { version = "4.3", default-features = false, optional = true } + +[dev-dependencies] +tokio.workspace = true +tower = { version = "0.5", features = ["util"] } +http-body-util = "0.1" + +[[bin]] +name = "ruview-nlos" +path = "src/bin/ruview-nlos.rs" +required-features = ["server"] + +[lints.rust] +unsafe_code = "forbid" diff --git a/v2/crates/ruview-nlos/README.md b/v2/crates/ruview-nlos/README.md new file mode 100644 index 00000000..5ed31745 --- /dev/null +++ b/v2/crates/ruview-nlos/README.md @@ -0,0 +1,76 @@ +# RuView Consumer NLOS + +`ruview-nlos` is the RuView Labs G0 optical-transient scaffold for consumer time-of-flight sensors. It preserves zone-level photon timing histograms, consumes externally estimated sensor pose, evaluates an approximate canonical rigid-object likelihood with a particle filter, applies a CSI prior only in synthetic regression, and emits one bounded hidden-target posterior for RuVector, RuField, WorldGraph, Swift, and browser consumers. + +This is not an ARKit depth map adapter. A depth map has already discarded the delayed multipath timing signal required for around the corner inversion. `TransientFrame::validate` rejects `depth_only` as live NLOS evidence. + +## Reproduction boundary + +The first physical research path is the public MIT consumer NLOS implementation at commit `15314de422a765a2d1b72ea7037dfafb2f908d7c`, used with the ST P NUCLEO 53L8A1 kit and VL53L8CH histogram output. RuView independently implements the documented STM32 row framing and 13-value configuration packet. The Rust preprocessing/scorer is a bounded synthetic architecture approximation, not numerical equivalence to the upstream 128-bin O'Toole resampling and calibration pipeline. Physical reproduction must run the pinned upstream path and the preregistered witness protocol. + +The software path has four evidence classes: + +| Input | Output ceiling | Meaning | +|---|---:|---| +| Deterministic generator | `l0_synthetic` | Software and performance regression only | +| Raw live histogram | `l1_measured` | Sensor bytes received, calibration not yet witnessed | +| Raw histogram plus bound empty room calibration | `l2_calibrated` | Measured optical posterior with calibration digest | + +The v1 wire contract deliberately rejects `l3_corroborated`: it cannot retain both modality lineages. Measured CSI fusion is unavailable in v1; only a scope-bound synthetic L0 prior is accepted for architecture tests. A future contract must carry authenticated optical/RF lineage and coordinate bindings before measured fusion can be enabled. + +Only the physical protocol in `docs/research/consumer-nlos-acceptance-protocol.md` can establish the hardware reproduction and fusion acceptance gates. + +## Pipeline + +1. `StAsciiDecoder` reads explicit USB serial rows and keeps all 8 to 128 timing bins for at most 64 zones. +2. `Calibration` averages an empty room, finds each direct wall peak, binds the result with SHA 256, subtracts background, masks the direct return, and maps time to uniform squared distance bins. +3. `MotionApertureTracker` retains up to 32 pose-tagged frames, estimates a bounded translational velocity in metres per second from monotonic frame time, back-warps moving hypotheses across the aperture, and evaluates 64 to 20,000 particles against a bounded canonical point cloud. +4. `CsiSpatialPrior` contributes a coarse Gaussian prior only when calibrated, finite, and fresh. Stale priors fail closed. +5. `TemporalFeatureMemory`, `RuFieldObservation`, and `WorldGraphUpdate` remove raw histograms and preserve confidence, evidence, calibration, and expiry. +6. `NlosHub` publishes an authenticated read-only HTTP/WebSocket surface. Production TLS is terminated by the required trusted reverse proxy. Native clients use a bearer header. Browsers exchange the bearer token for a 30 second, single-use, origin-bound ticket. + +## Commands + +```bash +cd v2 + +# Pure core, including deterministic acceptance tests +cargo test -p ruview-nlos --no-default-features + +# Authenticated server and WebSocket tests +cargo test -p ruview-nlos + +# Direct ST serial adapter compile and tests +cargo test -p ruview-nlos --all-features + +# L0 architecture benchmark. This never sets the hardware gate to true. +cargo run -p ruview-nlos --release -- benchmark --frames 300 --particles 1000 + +# Track a bounded transient JSONL recording. Its first 60 frames must be empty room. +cargo run -p ruview-nlos -- track-jsonl capture.jsonl --background-frames 60 + +# Read the public STM32 firmware at 2,250,000 baud and emit raw transient JSONL. +cargo run -p ruview-nlos --features hardware -- capture-st \ + --port /dev/ttyACM0 --session lab-run-001 --frames 300 \ + --sensor-id st-kit-001 --sensor-model VL53L8CH \ + --firmware-version 15314de --pose-jsonl synchronized-poses.jsonl + +# Run a loopback synthetic server for UI validation. +RUVIEW_NLOS_TOKEN="$(openssl rand -hex 32)" \ + cargo run -p ruview-nlos -- serve --synthetic \ + --allowed-origin http://127.0.0.1:8081 +``` + +Non loopback bind requires `--behind-tls-proxy`; the flag is an operator assertion, not a TLS implementation. The proxy must terminate trusted TLS, strip untrusted forwarded headers, and apply network policy. Browser CORS is disabled unless one exact `--allowed-origin` is supplied; wildcard origins and cleartext non-loopback origins are rejected. The bearer value is hashed immediately and is never stored or logged. + +## Performance model + +The reference configuration is 16 zones by 48 bins by 1,000 particles. The optimized scorer does not materialize a full predicted histogram per particle. It projects only the three nonzero kernel samples around each canonical return, reducing the point target case from roughly 768,000 to 48,000 predicted sample operations per frame and bounding the aperture at eight frames by default. + +`--fixed-sensor` is available only for bounded capture/transport diagnostics. Target motion does not substitute for the sensor-motion-induced aperture and a fixed-sensor capture cannot satisfy the physical MAS reproduction gate. The current pose JSONL is index-paired G0 offline scaffolding; live promotion needs timestamped pose/capture identity and clock synchronization. + +CI requires at least 30 combined tracker updates per second and at least 25 percent synthetic lost track reduction from the CSI prior. Those are software regression gates. The published MIT result and the RuView field acceptance criteria remain separate hardware claims. + +## Privacy and safety + +Raw histograms stay in the local capture and calibration plane. Public track envelopes contain session local identifiers only and expire in at most five seconds. UNKNOWN tracks are first class. Neither client nor server can actuate a device. The threat model is in `docs/security/consumer-nlos-threat-model.md`. diff --git a/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs b/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs new file mode 100644 index 00000000..1e55fe68 --- /dev/null +++ b/v2/crates/ruview-nlos/src/bin/ruview-nlos.rs @@ -0,0 +1,496 @@ +//! RuView NLOS research CLI. Hardware evidence requires the external witness +//! protocol; this binary never promotes its synthetic benchmark. + +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::time::Duration; + +use clap::{Parser, Subcommand}; +use ruview_nlos::calibration::{Calibration, CalibrationConfig}; +use ruview_nlos::server::NlosHub; +use ruview_nlos::{ + CanonicalObject, FrameSource, MotionApertureTracker, SyntheticScene, TrackEnvelope, + TrackerConfig, TransientFrame, TransientKind, Vec3, MAX_WIRE_BYTES, +}; +#[cfg(feature = "hardware")] +use ruview_nlos::{EvidenceLevel, Provenance, SensorPose, StAsciiDecoder, StDecoderConfig}; + +#[derive(Parser)] +#[command( + name = "ruview-nlos", + version, + about = "Consumer transient NLOS research runtime" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Run the deterministic L0 performance and fusion benchmark. + Benchmark { + /// Number of frames. + #[arg(long, default_value_t = 300)] + frames: usize, + /// Particle count. + #[arg(long, default_value_t = 1_000)] + particles: usize, + }, + /// Validate every bounded track JSONL record. + ValidateTrack { + /// JSONL file. + input: PathBuf, + }, + /// Process transient JSONL; the initial frames form empty-room calibration. + TrackJsonl { + /// Transient JSONL file. + input: PathBuf, + /// Empty-room frames at the beginning of the file. + #[arg(long, default_value_t = 60)] + background_frames: usize, + }, + /// Capture the public VL53L8CH STM32 stream directly over USB serial. + #[cfg(feature = "hardware")] + CaptureSt { + /// Explicit serial device path; auto-discovery is intentionally avoided. + #[arg(long)] + port: String, + /// Capture session identifier. + #[arg(long)] + session: String, + /// Stable enrolled sensor identifier; never inferred from a port path. + #[arg(long)] + sensor_id: String, + /// Exact sensor model. + #[arg(long, default_value = "VL53L8CH")] + sensor_model: String, + /// Exact flashed firmware revision or digest label. + #[arg(long)] + firmware_version: String, + /// One externally estimated sensor pose per output frame as JSONL. + #[arg(long, conflicts_with = "fixed_sensor")] + pose_jsonl: Option, + /// Explicitly declare a fixed identity pose. This does not synthesize + /// camera motion; aperture diversity must then come from target motion. + #[arg(long, conflicts_with = "pose_jsonl")] + fixed_sensor: bool, + /// Frames to emit as transient JSONL. + #[arg(long, default_value_t = 300)] + frames: usize, + /// Grid height. + #[arg(long, default_value_t = 4)] + height: u16, + /// Grid width. + #[arg(long, default_value_t = 4)] + width: u16, + /// Histogram bins. + #[arg(long, default_value_t = 48)] + bins: u16, + /// Firmware start bin. + #[arg(long, default_value_t = 30)] + start_bin: u16, + /// Requested ranging rate. + #[arg(long, default_value_t = 30)] + frequency_hz: u16, + }, + /// Serve authenticated native and browser clients. + Serve { + /// Bind address. Non-loopback requires an explicit reverse-proxy flag. + #[arg(long, default_value = "127.0.0.1:8787")] + bind: SocketAddr, + /// Environment variable holding a bearer token of at least 32 characters. + #[arg(long, default_value = "RUVIEW_NLOS_TOKEN")] + token_env: String, + /// Publish deterministic L0 frames at 30 Hz for UI validation. + #[arg(long)] + synthetic: bool, + /// Confirm that a TLS reverse proxy and network policy protect non-loopback bind. + #[arg(long)] + behind_tls_proxy: bool, + /// One exact HTTPS browser origin, or HTTP loopback origin for development. + #[arg(long)] + allowed_origin: Option, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt().with_target(false).init(); + match Cli::parse().command { + Command::Benchmark { frames, particles } => { + if !(1..=10_000).contains(&frames) || !(64..=20_000).contains(&particles) { + return Err("frames or particles outside bounded range".into()); + } + let report = SyntheticScene::benchmark(frames, particles); + println!("{}", serde_json::to_string_pretty(&report)?); + } + Command::ValidateTrack { input } => { + let count = read_jsonl::( + &input, + |frame| frame.validate(), + )?; + println!("validated {count} bounded track frames"); + } + Command::TrackJsonl { + input, + background_frames, + } => { + let frames = load_transients(&input)?; + if background_frames < 2 || background_frames >= frames.len() { + return Err("background-frames must leave at least one tracking frame".into()); + } + let calibration = Calibration::from_background( + &frames[..background_frames], + CalibrationConfig::default(), + )?; + let mut tracker = MotionApertureTracker::new( + calibration, + CanonicalObject::point(), + TrackerConfig::default(), + )?; + for frame in &frames[background_frames..] { + let output = tracker.update(frame, None)?; + println!("{}", serde_json::to_string(&output)?); + } + } + #[cfg(feature = "hardware")] + Command::CaptureSt { + port, + session, + sensor_id, + sensor_model, + firmware_version, + pose_jsonl, + fixed_sensor, + frames, + height, + width, + bins, + start_bin, + frequency_hz, + } => capture_st( + &port, + &session, + &sensor_id, + &sensor_model, + &firmware_version, + pose_jsonl.as_ref(), + fixed_sensor, + frames, + height, + width, + bins, + start_bin, + frequency_hz, + )?, + Command::Serve { + bind, + token_env, + synthetic, + behind_tls_proxy, + allowed_origin, + } => { + if !is_loopback(bind.ip()) && !behind_tls_proxy { + return Err("non-loopback bind requires --behind-tls-proxy".into()); + } + let token = std::env::var(&token_env).map_err(|_| { + format!("required token environment variable {token_env} is absent") + })?; + let server_session = if synthetic { + "synthetic-nlos-1" + } else { + "ruview-nlos-server" + }; + let mut hub = NlosHub::new(&token, server_session)?; + if let Some(origin) = allowed_origin { + hub = hub.with_allowed_origin(&origin)?; + } + if synthetic { + spawn_synthetic(hub.clone()); + } + let listener = tokio::net::TcpListener::bind(bind).await?; + tracing::info!(%bind, synthetic, "RuView NLOS server listening"); + axum::serve(listener, hub.router()).await?; + } + } + Ok(()) +} + +#[cfg(feature = "hardware")] +#[allow(clippy::too_many_arguments)] +fn capture_st( + port: &str, + session: &str, + sensor_id: &str, + sensor_model: &str, + firmware_version: &str, + pose_jsonl: Option<&PathBuf>, + fixed_sensor: bool, + frame_limit: usize, + height: u16, + width: u16, + bins: u16, + start_bin: u16, + frequency_hz: u16, +) -> Result<(), Box> { + use std::io::Write; + use std::time::{Instant, SystemTime, UNIX_EPOCH}; + + if !(1..=1_000_000).contains(&frame_limit) || !(1..=30).contains(&frequency_hz) { + return Err("capture frame or frequency bound exceeded".into()); + } + if pose_jsonl.is_some() == fixed_sensor { + return Err("choose exactly one of --pose-jsonl or --fixed-sensor".into()); + } + for value in [session, sensor_id, sensor_model, firmware_version] { + if !valid_cli_label(value) { + return Err("session and provenance labels must use 1..64 safe characters".into()); + } + } + let poses = if let Some(path) = pose_jsonl { + let poses = load_sensor_poses(path)?; + if poses.len() != frame_limit { + return Err("pose JSONL must contain exactly one pose per captured frame".into()); + } + Some(poses) + } else { + None + }; + let mut decoder = StAsciiDecoder::new(StDecoderConfig { + session_id: session.into(), + height, + width, + num_bins: bins, + start_bin, + bin_width_ps: 250.0, + fov_x_degrees: 45.0, + fov_y_degrees: 45.0, + add_back_ambient: false, + require_frame_marker: true, + source: FrameSource::Live, + evidence_level: EvidenceLevel::L1Measured, + calibration_hash: "0".repeat(64), + provenance: Provenance { + sensor_id: sensor_id.into(), + sensor_model: sensor_model.into(), + firmware_version: firmware_version.into(), + transient_kind: TransientKind::CompactNormalizedHistogram, + histogram_preserved: true, + transport: "usb_serial".into(), + }, + })?; + let mut serial = serialport::new(port, 2_250_000) + .timeout(Duration::from_secs(1)) + .open()?; + std::thread::sleep(Duration::from_secs(1)); + serial.clear(serialport::ClearBuffer::Input)?; + serial.write_all(&decoder.firmware_config_bytes(1, frequency_hz, 10, 1))?; + serial.flush()?; + std::thread::sleep(Duration::from_secs(1)); + serial.clear(serialport::ClearBuffer::Input)?; + let started = Instant::now(); + let mut reader = BufReader::new(serial); + let mut emitted = 0_usize; + while emitted < frame_limit { + let line = match read_bounded_line(&mut reader, 4_096) { + Ok(None) => continue, + Ok(Some(line)) => line, + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock + ) => + { + continue; + } + Err(error) => return Err(error.into()), + }; + let line = std::str::from_utf8(&line)?; + let captured_at_unix_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64; + if let Some(frame) = decoder.push_line( + line, + captured_at_unix_ms, + started.elapsed().as_nanos() as u64, + poses + .as_ref() + .map_or_else(SensorPose::default, |values| values[emitted]), + )? { + println!("{}", serde_json::to_string(&frame)?); + emitted += 1; + } + } + Ok(()) +} + +fn is_loopback(ip: IpAddr) -> bool { + ip.is_loopback() +} + +#[cfg(feature = "hardware")] +fn valid_cli_label(value: &str) -> bool { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':')) +} + +#[cfg(feature = "hardware")] +fn load_sensor_poses(path: &PathBuf) -> Result, Box> { + let mut poses = Vec::new(); + read_jsonl::(path, |pose| { + pose.validate()?; + poses.push(*pose); + Ok(()) + })?; + Ok(poses) +} + +fn spawn_synthetic(hub: NlosHub) { + tokio::spawn(async move { + let mut scene = SyntheticScene::default(); + let Ok(calibration) = Calibration::from_background( + &scene.background_frames(60), + CalibrationConfig::default(), + ) else { + return; + }; + let Ok(mut tracker) = MotionApertureTracker::new( + calibration, + CanonicalObject::point(), + TrackerConfig::default(), + ) else { + return; + }; + let mut sequence = 100_u64; + let mut interval = tokio::time::interval(Duration::from_millis(33)); + loop { + interval.tick().await; + let t = sequence as f32 * 0.02; + let target = Vec3::new(0.25 * t.sin(), 0.15 * (t * 0.7).cos(), 1.0); + let mut frame = scene.frame(Some(target), 1.0, sequence); + frame.captured_at_unix_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.as_millis() as u64); + if let Ok(output) = tracker.update(&frame, None) { + let _ = hub.publish(output).await; + } + sequence = sequence.saturating_add(1); + } + }); +} + +fn load_transients(path: &PathBuf) -> Result, Box> { + let mut frames = Vec::new(); + read_jsonl::(path, |frame| { + frames.push(as_offline_replay(frame.clone())?); + Ok(()) + })?; + Ok(frames) +} + +fn as_offline_replay( + mut frame: TransientFrame, +) -> Result { + frame.validate()?; + if frame.source == FrameSource::Live { + frame.source = FrameSource::Replay; + frame.provenance.transient_kind = TransientKind::Replay; + frame.provenance.transport = "replay".into(); + frame.provenance.histogram_preserved = true; + } + frame.validate()?; + Ok(frame) +} + +fn read_jsonl( + path: &PathBuf, + mut validate: impl FnMut(&T) -> Result<(), E>, +) -> Result> +where + T: serde::de::DeserializeOwned, + E: std::error::Error + 'static, +{ + let metadata = std::fs::metadata(path)?; + if metadata.len() > 512 * 1024 * 1024 { + return Err("JSONL input exceeds 512 MiB bound".into()); + } + let mut reader = BufReader::new(File::open(path)?); + let mut count = 0_usize; + while let Some(line) = read_bounded_line(&mut reader, MAX_WIRE_BYTES)? { + let line = std::str::from_utf8(&line)?; + if line.trim().is_empty() { + continue; + } + let value: T = serde_json::from_str(line)?; + validate(&value)?; + count += 1; + } + Ok(count) +} + +fn read_bounded_line( + reader: &mut R, + maximum_bytes: usize, +) -> std::io::Result>> { + let mut line = Vec::with_capacity(maximum_bytes.min(8 * 1024)); + loop { + let available = reader.fill_buf()?; + if available.is_empty() { + return Ok((!line.is_empty()).then_some(line)); + } + let newline = available.iter().position(|byte| *byte == b'\n'); + let payload = newline.unwrap_or(available.len()); + if line.len().saturating_add(payload) > maximum_bytes { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "line exceeds bounded parser limit", + )); + } + line.extend_from_slice(&available[..payload]); + let consumed = payload + usize::from(newline.is_some()); + reader.consume(consumed); + if newline.is_some() { + return Ok(Some(line)); + } + } +} + +#[cfg(test)] +mod tests { + use super::{as_offline_replay, read_bounded_line}; + use ruview_nlos::{EvidenceLevel, FrameSource, SyntheticScene, TransientKind, Vec3}; + use std::io::Cursor; + + #[test] + fn bounded_reader_rejects_before_growing_past_limit() { + let mut reader = Cursor::new(vec![b'x'; 65]); + let error = read_bounded_line(&mut reader, 64).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn bounded_reader_accepts_exact_limit_and_multiple_lines() { + let mut reader = Cursor::new(b"1234\nok\n".to_vec()); + assert_eq!(read_bounded_line(&mut reader, 4).unwrap().unwrap(), b"1234"); + assert_eq!(read_bounded_line(&mut reader, 4).unwrap().unwrap(), b"ok"); + assert!(read_bounded_line(&mut reader, 4).unwrap().is_none()); + } + + #[test] + fn offline_live_capture_is_relabelled_as_replay() { + let mut scene = SyntheticScene::default(); + let mut frame = scene.frame(Some(Vec3::new(0.0, 0.0, 1.0)), 1.0, 7); + frame.source = FrameSource::Live; + frame.evidence_level = EvidenceLevel::L1Measured; + frame.provenance.transient_kind = TransientKind::CompactNormalizedHistogram; + frame.provenance.transport = "usb_serial".into(); + let replay = as_offline_replay(frame).unwrap(); + assert_eq!(replay.source, FrameSource::Replay); + assert_eq!(replay.provenance.transient_kind, TransientKind::Replay); + assert_eq!(replay.provenance.transport, "replay"); + } +} diff --git a/v2/crates/ruview-nlos/src/bridge.rs b/v2/crates/ruview-nlos/src/bridge.rs new file mode 100644 index 00000000..b8c3b54a --- /dev/null +++ b/v2/crates/ruview-nlos/src/bridge.rs @@ -0,0 +1,377 @@ +//! Bounded adapters for RuVector temporal features, RuField observations, and +//! WorldGraph updates. These are data-plane outputs, not authority to mutate a +//! remote graph; callers remain responsible for tenant and OAuth policy. + +use std::collections::VecDeque; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use crate::protocol::{ + EvidenceLevel, FrameSource, ModalityContributions, Provenance, TrackEnvelope, TrackState, Vec3, +}; + +/// One compact trajectory feature suitable for insertion into a RuVector +/// temporal index. It contains no civil identity or raw photon histogram. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TemporalFeature { + /// Capture session. + pub session_id: String, + /// Session-local track id. + pub track_id: String, + /// Capture time. + pub at_unix_ms: u64, + /// Fixed position, velocity, covariance, confidence, and modality vector. + pub embedding: [f32; 12], + /// Evidence ceiling retained with the vector. + pub evidence_level: EvidenceLevel, + /// Live/replay/synthetic source retained with the vector. + pub source: FrameSource, + /// Algorithm revision retained with the vector. + pub algorithm_version: String, + /// Calibration digest retained with the vector. + pub calibration_hash: String, + /// Sensor and transport provenance retained with the vector. + pub provenance: Provenance, + /// Hard expiry. + pub expires_at_unix_ms: u64, +} + +/// In-process deterministic reference memory. Production deployments can write +/// the same [`TemporalFeature`] records to tenant-scoped RuVector storage. +pub struct TemporalFeatureMemory { + capacity: usize, + records: VecDeque, +} + +impl TemporalFeatureMemory { + /// Construct a bounded memory. + pub fn new(capacity: usize) -> Result { + if !(1..=65_536).contains(&capacity) { + return Err(BridgeError::InvalidCapacity); + } + Ok(Self { + capacity, + records: VecDeque::with_capacity(capacity.min(1_024)), + }) + } + + /// Insert every non-UNKNOWN track after validating its envelope. + pub fn observe(&mut self, envelope: &TrackEnvelope) -> Result { + envelope.validate()?; + self.records + .retain(|record| record.expires_at_unix_ms >= envelope.captured_at_unix_ms); + let mut inserted = 0_usize; + for track in envelope + .tracks + .iter() + .filter(|track| track.state != TrackState::Unknown) + { + while self.records.len() >= self.capacity { + self.records.pop_front(); + } + self.records.push_back(TemporalFeature { + session_id: envelope.session_id.clone(), + track_id: track.track_id.clone(), + at_unix_ms: envelope.captured_at_unix_ms, + embedding: [ + track.position_m.x, + track.position_m.y, + track.position_m.z, + track.velocity_mps.x, + track.velocity_mps.y, + track.velocity_mps.z, + track.covariance_diagonal_m2.x, + track.covariance_diagonal_m2.y, + track.covariance_diagonal_m2.z, + track.confidence, + track.modality_contributions.lidar, + track.modality_contributions.csi, + ], + evidence_level: envelope.evidence_level, + source: envelope.source, + algorithm_version: envelope.algorithm_version.clone(), + calibration_hash: envelope.calibration_hash.clone(), + provenance: envelope.provenance.clone(), + expires_at_unix_ms: envelope.expires_at_unix_ms, + }); + inserted += 1; + } + Ok(inserted) + } + + /// Return the closest same-session feature by cosine similarity. + #[must_use] + pub fn nearest( + &self, + query: &[f32; 12], + session_id: &str, + now_unix_ms: u64, + ) -> Option<(&TemporalFeature, f32)> { + if query.iter().any(|value| !value.is_finite()) { + return None; + } + self.records + .iter() + .filter(|record| record.session_id == session_id) + .filter(|record| record.expires_at_unix_ms > now_unix_ms) + .filter_map(|record| cosine(query, &record.embedding).map(|score| (record, score))) + .max_by(|a, b| a.1.total_cmp(&b.1)) + } + + /// Current bounded record count. + #[must_use] + pub fn len(&self) -> usize { + self.records.len() + } + + /// Whether no records remain. + #[must_use] + pub fn is_empty(&self) -> bool { + self.records.is_empty() + } + + /// Remove all records that are no longer fresh. + pub fn purge_expired(&mut self, now_unix_ms: u64) -> usize { + let before = self.records.len(); + self.records + .retain(|record| record.expires_at_unix_ms > now_unix_ms); + before - self.records.len() + } +} + +fn cosine(a: &[f32; 12], b: &[f32; 12]) -> Option { + let mut dot = 0.0; + let mut aa = 0.0; + let mut bb = 0.0; + for (left, right) in a.iter().zip(b.iter()) { + dot += left * right; + aa += left * left; + bb += right * right; + } + let denominator = (aa * bb).sqrt(); + (denominator > f32::EPSILON).then(|| (dot / denominator).clamp(-1.0, 1.0)) +} + +/// Privacy-safe field observation emitted for every quality-gated track. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RuFieldObservation { + /// Field schema. + pub schema: String, + /// Session-local observation id. + pub observation_id: String, + /// World position. + pub position_m: Vec3, + /// Diagonal spatial covariance. + pub covariance_diagonal_m2: Vec3, + /// Confidence. + pub confidence: f32, + /// Current signal quality. + pub signal_quality: f32, + /// Observable modality weights. + pub modality_contributions: ModalityContributions, + /// Capture time. + pub observed_at_unix_ms: u64, + /// Hard expiry. + pub expires_at_unix_ms: u64, + /// Evidence ceiling. + pub evidence_level: EvidenceLevel, + /// Source watermark. + pub source: FrameSource, + /// Calibration digest. + pub calibration_hash: String, + /// Algorithm revision. + pub algorithm_version: String, + /// Sensor/signal/transport provenance. + pub provenance: Provenance, +} + +impl RuFieldObservation { + /// Convert all non-UNKNOWN tracks without retaining raw histograms. + pub fn from_envelope(envelope: &TrackEnvelope) -> Result, BridgeError> { + envelope.validate()?; + Ok(envelope + .tracks + .iter() + .filter(|track| track.state != TrackState::Unknown) + .map(|track| Self { + schema: "rufield.observation.nlos.v1".into(), + observation_id: canonical_id( + b"rufield-observation-v1", + &[ + envelope.session_id.as_bytes(), + track.track_id.as_bytes(), + &envelope.sequence.to_le_bytes(), + ], + ), + position_m: track.position_m, + covariance_diagonal_m2: track.covariance_diagonal_m2, + confidence: track.confidence, + signal_quality: track.signal_quality, + modality_contributions: track.modality_contributions, + observed_at_unix_ms: envelope.captured_at_unix_ms, + expires_at_unix_ms: envelope.expires_at_unix_ms, + evidence_level: envelope.evidence_level, + source: envelope.source, + calibration_hash: envelope.calibration_hash.clone(), + algorithm_version: envelope.algorithm_version.clone(), + provenance: envelope.provenance.clone(), + }) + .collect()) + } +} + +/// Minimal idempotent WorldGraph mutation request. A governed writer applies it +/// only after tenant authorization and evidence policy checks. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldGraphUpdate { + /// Mutation schema. + pub schema: String, + /// Idempotency key. + pub idempotency_key: String, + /// Pseudonymous node id. + pub node_id: String, + /// Node kind. + pub node_kind: String, + /// World position. + pub position_m: Vec3, + /// Estimated velocity. + pub velocity_mps: Vec3, + /// Diagonal spatial covariance. + pub covariance_diagonal_m2: Vec3, + /// Posterior confidence. + pub confidence: f32, + /// Current signal quality. + pub signal_quality: f32, + /// Observable modality weights. + pub modality_contributions: ModalityContributions, + /// Quality-gated state. + pub state: TrackState, + /// Evidence ceiling. + pub evidence_level: EvidenceLevel, + /// Live/replay/synthetic source. + pub source: FrameSource, + /// Observation time. + pub observed_at_unix_ms: u64, + /// Calibration digest. + pub calibration_hash: String, + /// Algorithm revision. + pub algorithm_version: String, + /// Sensor/signal/transport provenance. + pub provenance: Provenance, + /// Hard expiry. + pub expires_at_unix_ms: u64, +} + +impl WorldGraphUpdate { + /// Convert tracks into idempotent, tenant-neutral mutation requests. + pub fn from_envelope(envelope: &TrackEnvelope) -> Result, BridgeError> { + envelope.validate()?; + Ok(envelope + .tracks + .iter() + .map(|track| Self { + schema: "worldgraph.update.nlos.v1".into(), + idempotency_key: canonical_id( + b"worldgraph-update-v1", + &[ + envelope.session_id.as_bytes(), + &envelope.sequence.to_le_bytes(), + track.track_id.as_bytes(), + ], + ), + node_id: canonical_id( + b"worldgraph-node-v1", + &[envelope.session_id.as_bytes(), track.track_id.as_bytes()], + ), + node_kind: "hidden_target_hypothesis".into(), + position_m: track.position_m, + velocity_mps: track.velocity_mps, + covariance_diagonal_m2: track.covariance_diagonal_m2, + confidence: track.confidence, + signal_quality: track.signal_quality, + modality_contributions: track.modality_contributions, + state: track.state, + evidence_level: envelope.evidence_level, + source: envelope.source, + observed_at_unix_ms: envelope.captured_at_unix_ms, + calibration_hash: envelope.calibration_hash.clone(), + algorithm_version: envelope.algorithm_version.clone(), + provenance: envelope.provenance.clone(), + expires_at_unix_ms: envelope.expires_at_unix_ms, + }) + .collect()) + } +} + +fn canonical_id(domain: &[u8], fields: &[&[u8]]) -> String { + let mut digest = Sha256::new(); + digest.update(domain); + digest.update([0]); + for field in fields { + digest.update((field.len() as u32).to_le_bytes()); + digest.update(field); + } + format!("nlos-{:x}", digest.finalize()) +} + +/// Bridge conversion failure. +#[derive(Debug, Error)] +pub enum BridgeError { + /// Memory capacity was zero or unreasonably large. + #[error("invalid temporal memory capacity")] + InvalidCapacity, + /// Source track contract was invalid. + #[error(transparent)] + Contract(#[from] crate::protocol::ContractError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::simulator::SyntheticScene; + + #[test] + fn bridge_retains_evidence_and_never_raw_histograms() { + let report = SyntheticScene::benchmark(2, 64); + assert_eq!(report.evidence, "SYNTHETIC_L0"); + let memory = TemporalFeatureMemory::new(8).unwrap(); + assert!(memory.is_empty()); + let serialized = serde_json::to_string(&RuFieldObservation { + schema: "rufield.observation.nlos.v1".into(), + observation_id: "s:t:1".into(), + position_m: Vec3::default(), + covariance_diagonal_m2: Vec3::default(), + confidence: 0.0, + signal_quality: 0.0, + modality_contributions: ModalityContributions { + lidar: 1.0, + csi: 0.0, + }, + observed_at_unix_ms: 1, + expires_at_unix_ms: 2, + evidence_level: EvidenceLevel::L0Synthetic, + source: FrameSource::Synthetic, + calibration_hash: "0".repeat(64), + algorithm_version: "test-v1".into(), + provenance: crate::protocol::Provenance { + sensor_id: "sim".into(), + sensor_model: "sim".into(), + firmware_version: "sim".into(), + transient_kind: crate::protocol::TransientKind::Replay, + histogram_preserved: false, + transport: "replay".into(), + }, + }) + .unwrap(); + // The provenance boolean is retained, but neither histogram bins nor + // raw zones cross this bridge. + assert!(serialized.contains("histogramPreserved")); + assert!(!serialized.contains("\"histogram\":")); + assert!(!serialized.contains("\"zones\":")); + } +} diff --git a/v2/crates/ruview-nlos/src/calibration.rs b/v2/crates/ruview-nlos/src/calibration.rs new file mode 100644 index 00000000..fc7f86c5 --- /dev/null +++ b/v2/crates/ruview-nlos/src/calibration.rs @@ -0,0 +1,534 @@ +//! Empty-room calibration, direct-return removal, and light-cone resampling. + +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use crate::protocol::{EvidenceLevel, FrameSource, Provenance, SensorPose, TransientFrame, Vec3}; + +/// Preprocessing controls matching the public consumer-NLOS workflow. +#[derive(Clone, Copy, Debug)] +pub struct CalibrationConfig { + /// Bins masked on either side of the direct wall return. + pub pulse_half_width: usize, + /// Earliest extra-path bins discarded after direct-return alignment. + pub zero_first_bins: usize, + /// Maximum empty-room frames accepted into one calibration. + pub max_background_frames: usize, + /// Maximum sensor translation from the empty-room capture centroid. + pub max_sensor_translation_m: f32, + /// Maximum per-zone relay point displacement in the world frame. + pub max_wall_point_drift_m: f32, + /// Maximum per-zone reported range drift from calibration. + pub max_wall_distance_drift_m: f32, +} + +impl Default for CalibrationConfig { + fn default() -> Self { + Self { + pulse_half_width: 7, + zero_first_bins: 15, + max_background_frames: 256, + max_sensor_translation_m: 0.5, + max_wall_point_drift_m: 0.5, + max_wall_distance_drift_m: 0.25, + } + } +} + +/// Immutable empty-room calibration bound by a deterministic SHA-256 digest. +#[derive(Clone, Debug)] +pub struct Calibration { + /// Configuration used to build the calibration. + config: CalibrationConfig, + /// Session in which the empty room was captured. + session_id: String, + /// Homogeneous source classification of the calibration capture. + source: FrameSource, + /// Evidence ceiling of the calibration inputs. + input_evidence_level: EvidenceLevel, + /// Full sensor/signal/transport provenance bound into the calibration. + provenance: Provenance, + /// Number of zones. + zone_count: usize, + /// Number of native timing bins. + bin_count: usize, + /// Timing width in picoseconds. + bin_width_ps: f32, + /// First firmware bin. + start_bin: u16, + /// Sensor identity bound into the calibration digest. + sensor_id: String, + /// Sensor model bound into the calibration digest. + sensor_model: String, + /// Firmware revision bound into the calibration digest. + firmware_version: String, + /// Number of background samples committed to the digest. + sample_count: usize, + /// Mean sensor translation during empty-room capture. + reference_sensor_translation_m: Vec3, + /// Average relay-wall points in sensor-local coordinates. + wall_points_m: Vec, + /// Average relay-wall points transformed into the world coordinate frame. + wall_points_world_m: Vec, + /// Average per-zone range reported during calibration. + wall_distances_m: Vec, + /// Peak direct-return index for each zone. + direct_peak_bins: Vec, + /// Empty-room mean counts, row-major by zone and bin. + background: Vec, + /// SHA-256 digest over all fields above. + hash: String, +} + +impl Calibration { + /// Build a deterministic empty-room calibration from two or more frames. + pub fn from_background( + frames: &[TransientFrame], + config: CalibrationConfig, + ) -> Result { + if frames.len() < 2 || frames.len() > config.max_background_frames { + return Err(CalibrationError::FrameCount); + } + if !config.max_sensor_translation_m.is_finite() + || !(0.01..=5.0).contains(&config.max_sensor_translation_m) + || !config.max_wall_point_drift_m.is_finite() + || !(0.01..=5.0).contains(&config.max_wall_point_drift_m) + || !config.max_wall_distance_drift_m.is_finite() + || !(0.01..=5.0).contains(&config.max_wall_distance_drift_m) + { + return Err(CalibrationError::InvalidConfig); + } + for frame in frames { + frame.validate()?; + } + let first = &frames[0]; + let zone_count = first.zones.len(); + let bin_count = first.zones[0].histogram.len(); + if config.pulse_half_width >= bin_count || config.zero_first_bins >= bin_count { + return Err(CalibrationError::InvalidConfig); + } + if frames.windows(2).any(|pair| { + pair[1].sequence <= pair[0].sequence + || pair[1].monotonic_ns <= pair[0].monotonic_ns + || pair[1].captured_at_unix_ms < pair[0].captured_at_unix_ms + }) { + return Err(CalibrationError::OutOfOrderFrames); + } + if frames.iter().any(|frame| { + frame.session_id != first.session_id + || frame.source != first.source + || frame.evidence_level != first.evidence_level + || frame.provenance != first.provenance + || frame.calibration_hash != first.calibration_hash + || frame.zones.len() != zone_count + || frame.zones[0].histogram.len() != bin_count + || frame.bin_width_ps != first.bin_width_ps + || frame.start_bin != first.start_bin + || frame.provenance.sensor_id != first.provenance.sensor_id + || frame.provenance.sensor_model != first.provenance.sensor_model + || frame.provenance.firmware_version != first.provenance.firmware_version + || frame + .zones + .iter() + .any(|zone| zone.histogram.len() != bin_count) + }) { + return Err(CalibrationError::InconsistentFrames); + } + + let mut background = vec![0.0_f32; zone_count * bin_count]; + let mut wall_points_m = vec![Vec3::default(); zone_count]; + let mut wall_points_world_m = vec![Vec3::default(); zone_count]; + let mut wall_distances_m = vec![0.0_f32; zone_count]; + let mut reference_sensor_translation_m = Vec3::default(); + for frame in frames { + reference_sensor_translation_m = + reference_sensor_translation_m.plus(frame.sensor_pose.translation_m); + for (zone_index, zone) in frame.zones.iter().enumerate() { + wall_points_m[zone_index] = wall_points_m[zone_index].plus(zone.wall_point_m); + wall_points_world_m[zone_index] = wall_points_world_m[zone_index] + .plus(frame.sensor_pose.transform(zone.wall_point_m)); + wall_distances_m[zone_index] += zone.distance_m; + for (bin, count) in zone.histogram.iter().enumerate() { + background[zone_index * bin_count + bin] += f32::from(*count); + } + } + } + let inv = 1.0 / frames.len() as f32; + for value in &mut background { + *value *= inv; + } + for point in &mut wall_points_m { + *point = point.scale(inv); + } + for point in &mut wall_points_world_m { + *point = point.scale(inv); + } + for distance in &mut wall_distances_m { + *distance *= inv; + } + reference_sensor_translation_m = reference_sensor_translation_m.scale(inv); + let direct_peak_bins = (0..zone_count) + .map(|zone| { + background[zone * bin_count..(zone + 1) * bin_count] + .iter() + .enumerate() + .max_by(|a, b| a.1.total_cmp(b.1)) + .map_or(0, |(index, _)| index) + }) + .collect::>(); + + let mut calibration = Self { + config, + session_id: first.session_id.clone(), + source: first.source, + input_evidence_level: first.evidence_level, + provenance: first.provenance.clone(), + zone_count, + bin_count, + bin_width_ps: first.bin_width_ps, + start_bin: first.start_bin, + sensor_id: first.provenance.sensor_id.clone(), + sensor_model: first.provenance.sensor_model.clone(), + firmware_version: first.provenance.firmware_version.clone(), + sample_count: frames.len(), + reference_sensor_translation_m, + wall_points_m, + wall_points_world_m, + wall_distances_m, + direct_peak_bins, + background, + hash: String::new(), + }; + calibration.hash = calibration.compute_hash(); + Ok(calibration) + } + + /// Stable calibration digest used by measured track envelopes. + #[must_use] + pub fn hash(&self) -> &str { + &self.hash + } + + /// Capture session bound into this calibration. + #[must_use] + pub fn session_id(&self) -> &str { + &self.session_id + } + + /// Number of calibrated relay zones. + #[must_use] + pub fn zone_count(&self) -> usize { + self.zone_count + } + + /// Background-subtract, mask the direct peak, align extra path time, and + /// resample from time to uniform squared-distance (light-cone) bins. + pub fn preprocess( + &self, + frame: &TransientFrame, + ) -> Result { + if self.compute_hash() != self.hash { + return Err(CalibrationError::IntegrityMismatch); + } + frame.validate()?; + if frame.session_id != self.session_id + || frame.source != self.source + || frame.provenance != self.provenance + || frame.zones.len() != self.zone_count + || frame.zones[0].histogram.len() != self.bin_count + || frame.bin_width_ps != self.bin_width_ps + || frame.start_bin != self.start_bin + || frame.provenance.sensor_id != self.sensor_id + || frame.provenance.sensor_model != self.sensor_model + || frame.provenance.firmware_version != self.firmware_version + { + return Err(CalibrationError::InconsistentFrames); + } + if frame.source != FrameSource::Synthetic + && frame.calibration_hash != self.hash + && frame.calibration_hash != "0".repeat(64) + { + return Err(CalibrationError::WrongCalibration); + } + + if frame + .sensor_pose + .translation_m + .distance(self.reference_sensor_translation_m) + > self.config.max_sensor_translation_m + || frame.zones.iter().enumerate().any(|(zone_index, zone)| { + frame + .sensor_pose + .transform(zone.wall_point_m) + .distance(self.wall_points_world_m[zone_index]) + > self.config.max_wall_point_drift_m + || (zone.distance_m - self.wall_distances_m[zone_index]).abs() + > self.config.max_wall_distance_drift_m + }) + { + return Err(CalibrationError::GeometryDrift); + } + + let mut light_cone_histograms = vec![0.0_f32; self.zone_count * self.bin_count]; + let mut foreground_sum = 0.0_f32; + let mut noise_floor = 0.0_f32; + let denominator = (self.bin_count - 1).max(1); + for (zone_index, zone) in frame.zones.iter().enumerate() { + let peak = self.direct_peak_bins[zone_index]; + let direct_end = peak.saturating_add(self.config.pulse_half_width); + for native_bin in 0..self.bin_count { + let baseline = self.background[zone_index * self.bin_count + native_bin]; + let residual = (f32::from(zone.histogram[native_bin]) - baseline).max(0.0); + noise_floor += baseline.sqrt().max(1.0); + if native_bin <= direct_end { + continue; + } + let extra_bin = native_bin - peak; + if extra_bin < self.config.zero_first_bins { + continue; + } + let v_bin = ((extra_bin * extra_bin) / denominator).min(self.bin_count - 1); + light_cone_histograms[zone_index * self.bin_count + v_bin] += residual; + foreground_sum += residual; + } + } + let signal_quality = foreground_sum / (foreground_sum + noise_floor.max(1.0)); + let wall_points_world_m = frame + .zones + .iter() + .map(|zone| frame.sensor_pose.transform(zone.wall_point_m)) + .collect(); + Ok(PreprocessedFrame { + sequence: frame.sequence, + captured_at_unix_ms: frame.captured_at_unix_ms, + monotonic_ns: frame.monotonic_ns, + source: frame.source, + evidence_level: frame.evidence_level, + sensor_pose: frame.sensor_pose, + wall_points_world_m, + light_cone_histograms, + zone_count: self.zone_count, + bin_count: self.bin_count, + bin_width_ps: self.bin_width_ps, + signal_quality: signal_quality.clamp(0.0, 1.0), + }) + } + + fn compute_hash(&self) -> String { + let mut digest = Sha256::new(); + digest.update(b"ruview.nlos.calibration.v1\0"); + update_string(&mut digest, &self.session_id); + digest.update([frame_source_code(self.source)]); + digest.update([evidence_level_code(self.input_evidence_level)]); + update_string(&mut digest, &self.provenance.sensor_id); + update_string(&mut digest, &self.provenance.sensor_model); + update_string(&mut digest, &self.provenance.firmware_version); + digest.update([transient_kind_code(self.provenance.transient_kind)]); + digest.update([u8::from(self.provenance.histogram_preserved)]); + update_string(&mut digest, &self.provenance.transport); + digest.update((self.zone_count as u64).to_le_bytes()); + digest.update((self.bin_count as u64).to_le_bytes()); + digest.update(self.bin_width_ps.to_le_bytes()); + digest.update(self.start_bin.to_le_bytes()); + digest.update((self.config.pulse_half_width as u64).to_le_bytes()); + digest.update((self.config.zero_first_bins as u64).to_le_bytes()); + digest.update((self.config.max_background_frames as u64).to_le_bytes()); + digest.update(self.config.max_sensor_translation_m.to_le_bytes()); + digest.update(self.config.max_wall_point_drift_m.to_le_bytes()); + digest.update(self.config.max_wall_distance_drift_m.to_le_bytes()); + digest.update((self.sample_count as u64).to_le_bytes()); + digest.update(self.reference_sensor_translation_m.x.to_le_bytes()); + digest.update(self.reference_sensor_translation_m.y.to_le_bytes()); + digest.update(self.reference_sensor_translation_m.z.to_le_bytes()); + for point in &self.wall_points_m { + digest.update(point.x.to_le_bytes()); + digest.update(point.y.to_le_bytes()); + digest.update(point.z.to_le_bytes()); + } + for point in &self.wall_points_world_m { + digest.update(point.x.to_le_bytes()); + digest.update(point.y.to_le_bytes()); + digest.update(point.z.to_le_bytes()); + } + for distance in &self.wall_distances_m { + digest.update(distance.to_le_bytes()); + } + for peak in &self.direct_peak_bins { + digest.update((*peak as u64).to_le_bytes()); + } + for value in &self.background { + digest.update(value.to_le_bytes()); + } + format!("{:x}", digest.finalize()) + } +} + +fn update_string(digest: &mut Sha256, value: &str) { + digest.update((value.len() as u32).to_le_bytes()); + digest.update(value.as_bytes()); +} + +fn frame_source_code(value: FrameSource) -> u8 { + match value { + FrameSource::Live => 1, + FrameSource::Replay => 2, + FrameSource::Synthetic => 3, + } +} + +fn evidence_level_code(value: EvidenceLevel) -> u8 { + match value { + EvidenceLevel::L0Synthetic => 0, + EvidenceLevel::L1Measured => 1, + EvidenceLevel::L2Calibrated => 2, + EvidenceLevel::L3Corroborated => 3, + } +} + +fn transient_kind_code(value: crate::protocol::TransientKind) -> u8 { + match value { + crate::protocol::TransientKind::RawHistogram => 1, + crate::protocol::TransientKind::CompactNormalizedHistogram => 2, + crate::protocol::TransientKind::DepthOnly => 3, + crate::protocol::TransientKind::Replay => 4, + } +} + +/// A frame ready for motion-induced aperture likelihood evaluation. +#[derive(Clone, Debug)] +pub struct PreprocessedFrame { + /// Input sequence. + pub sequence: u64, + /// UTC capture time. + pub captured_at_unix_ms: u64, + /// Monotonic capture time used for motion compensation. + pub monotonic_ns: u64, + /// Source label. + pub source: FrameSource, + /// Evidence level. + pub evidence_level: crate::protocol::EvidenceLevel, + /// Sensor pose used for this aperture sample. + pub sensor_pose: SensorPose, + /// Relay wall samples transformed into world coordinates. + pub wall_points_world_m: Vec, + /// Uniform squared-distance histogram, row-major by zone and bin. + pub light_cone_histograms: Vec, + /// Number of zones. + pub zone_count: usize, + /// Number of squared-distance bins. + pub bin_count: usize, + /// Native timing width in picoseconds. + pub bin_width_ps: f32, + /// Foreground-to-noise quality estimate. + pub signal_quality: f32, +} + +/// Calibration or preprocessing failure. +#[derive(Debug, Error)] +pub enum CalibrationError { + /// Too few or too many background frames. + #[error("background calibration requires 2..=max_background_frames frames")] + FrameCount, + /// Invalid pulse or early-bin mask configuration. + #[error("invalid calibration configuration")] + InvalidConfig, + /// Frames differ in session, shape, timing, or calibration. + #[error("inconsistent transient frames")] + InconsistentFrames, + /// Calibration frames repeated or moved backwards. + #[error("calibration frames must have strictly increasing sequence and monotonic time")] + OutOfOrderFrames, + /// A measured frame names a different calibration digest. + #[error("transient frame is bound to a different calibration")] + WrongCalibration, + /// The supposedly immutable calibration payload no longer matches its digest. + #[error("calibration integrity digest mismatch")] + IntegrityMismatch, + /// Sensor pose or relay geometry moved outside the calibrated operating volume. + #[error("transient geometry is outside the calibrated operating volume")] + GeometryDrift, + /// Raw frame contract violation. + #[error(transparent)] + Contract(#[from] crate::protocol::ContractError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::simulator::SyntheticScene; + + #[test] + fn calibration_is_deterministic_and_foreground_survives() { + let mut scene = SyntheticScene::default(); + let background = scene.background_frames(10); + let a = Calibration::from_background(&background, CalibrationConfig::default()).unwrap(); + let b = Calibration::from_background(&background, CalibrationConfig::default()).unwrap(); + assert_eq!(a.hash, b.hash); + let frame = scene.frame(Some(Vec3::new(0.2, 0.1, 1.0)), 1.0, 20); + let processed = a.preprocess(&frame).unwrap(); + assert!(processed.signal_quality > 0.0); + assert_eq!( + processed.light_cone_histograms.len(), + a.zone_count * a.bin_count + ); + } + + #[test] + fn calibration_rejects_duplicate_and_mixed_provenance_frames() { + let mut scene = SyntheticScene::default(); + let background = scene.background_frames(3); + let duplicate = vec![background[0].clone(), background[0].clone()]; + assert!(matches!( + Calibration::from_background(&duplicate, CalibrationConfig::default()), + Err(CalibrationError::OutOfOrderFrames) + )); + + let mut mixed = background[..2].to_vec(); + mixed[1].provenance.sensor_id = "another-sensor".into(); + assert!(matches!( + Calibration::from_background(&mixed, CalibrationConfig::default()), + Err(CalibrationError::InconsistentFrames) + )); + } + + #[test] + fn calibration_hash_uses_unambiguous_length_prefixed_identity() { + let mut scene = SyntheticScene::default(); + let mut left = scene.background_frames(2); + for frame in &mut left { + frame.provenance.sensor_id = "ab".into(); + frame.provenance.sensor_model = "c".into(); + } + let mut right = left.clone(); + for frame in &mut right { + frame.provenance.sensor_id = "a".into(); + frame.provenance.sensor_model = "bc".into(); + } + let left = Calibration::from_background(&left, CalibrationConfig::default()).unwrap(); + let right = Calibration::from_background(&right, CalibrationConfig::default()).unwrap(); + assert_ne!(left.hash, right.hash); + } + + #[test] + fn preprocessing_rejects_tampered_calibration_and_geometry_drift() { + let mut scene = SyntheticScene::default(); + let background = scene.background_frames(3); + let calibration = + Calibration::from_background(&background, CalibrationConfig::default()).unwrap(); + let frame = scene.frame(Some(Vec3::new(0.0, 0.0, 1.0)), 1.0, 10); + + let mut tampered = calibration.clone(); + tampered.background[0] += 1.0; + assert!(matches!( + tampered.preprocess(&frame), + Err(CalibrationError::IntegrityMismatch) + )); + + let mut drifted = frame; + drifted.sensor_pose.translation_m.x += 1.0; + assert!(matches!( + calibration.preprocess(&drifted), + Err(CalibrationError::GeometryDrift) + )); + } +} diff --git a/v2/crates/ruview-nlos/src/fusion.rs b/v2/crates/ruview-nlos/src/fusion.rs new file mode 100644 index 00000000..b02092e1 --- /dev/null +++ b/v2/crates/ruview-nlos/src/fusion.rs @@ -0,0 +1,231 @@ +//! Calibrated CSI spatial prior for optical NLOS particle fusion. + +use thiserror::Error; + +use crate::protocol::{EvidenceLevel, FrameSource, Vec3}; + +const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; + +/// Exact policy and coordinate scope required before two modalities may be +/// joined. The v1 track envelope cannot retain this complete lineage, so the +/// current tracker uses the binding only for L0 synthetic architecture tests +/// and rejects measured fusion until a lineage-preserving wire revision lands. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FusionScope { + /// Owning tenant. + pub tenant_id: String, + /// Owning workspace. + pub workspace_id: String, + /// Physical deployment site. + pub site_id: String, + /// Calibrated common coordinate frame. + pub world_frame_id: String, + /// Capture session shared by both modalities. + pub session_id: String, + /// Digest of the accepted CSI-to-world coordinate transform. + pub coordinate_transform_hash: String, +} + +impl FusionScope { + /// Validate bounded identifiers and the non-zero transform digest. + pub fn validate(&self) -> Result<(), FusionError> { + for value in [ + &self.tenant_id, + &self.workspace_id, + &self.site_id, + &self.world_frame_id, + &self.session_id, + ] { + if !valid_label(value) { + return Err(FusionError::InvalidBinding); + } + } + if !valid_hash(&self.coordinate_transform_hash) + || self.coordinate_transform_hash == "0".repeat(64) + { + return Err(FusionError::InvalidBinding); + } + Ok(()) + } +} + +/// A coarse RF spatial prior. CSI is never treated as centimetre-scale ground +/// truth; its covariance and confidence explicitly bound its influence. +#[derive(Clone, Debug)] +pub struct CsiSpatialPrior { + /// CSI source classification. + pub source: FrameSource, + /// Monotonic sequence within the CSI capture session. + pub sequence: u64, + /// UTC time of the RF observation. + pub captured_at_unix_ms: u64, + /// Tenant/session/world-frame binding for the temporal-spatial join. + pub scope: FusionScope, + /// Coarse region mean in the NLOS world frame. + pub mean_m: Vec3, + /// Diagonal covariance in square metres. + pub covariance_diagonal_m2: Vec3, + /// Bounded RF confidence. + pub confidence: f32, + /// Evidence level of the RF observation. + pub evidence_level: EvidenceLevel, + /// Authenticated RF sensor identifier. + pub sensor_id: String, + /// RF calibration digest. + pub calibration_hash: String, +} + +impl CsiSpatialPrior { + /// Validate all values before the prior can affect optical particles. + pub fn validate(&self) -> Result<(), FusionError> { + self.scope.validate()?; + if self.sequence > MAX_SAFE_INTEGER + || self.captured_at_unix_ms > MAX_SAFE_INTEGER + || !self.mean_m.finite() + || [self.mean_m.x, self.mean_m.y, self.mean_m.z] + .iter() + .any(|value| value.abs() > 100.0) + || !self.covariance_diagonal_m2.finite() + || [ + self.covariance_diagonal_m2.x, + self.covariance_diagonal_m2.y, + self.covariance_diagonal_m2.z, + ] + .iter() + .any(|value| !(0.0025..=25.0).contains(value)) + || !self.confidence.is_finite() + || !(0.0..=1.0).contains(&self.confidence) + { + return Err(FusionError::InvalidPrior); + } + if !valid_label(&self.sensor_id) || !valid_hash(&self.calibration_hash) { + return Err(FusionError::InvalidPrior); + } + if (self.source == FrameSource::Synthetic) + != (self.evidence_level == EvidenceLevel::L0Synthetic) + || (self.source == FrameSource::Synthetic && self.calibration_hash != "0".repeat(64)) + { + return Err(FusionError::EvidenceMismatch); + } + if self.source != FrameSource::Synthetic + && self.evidence_level == EvidenceLevel::L0Synthetic + { + return Err(FusionError::EvidenceMismatch); + } + if self.evidence_level >= EvidenceLevel::L2Calibrated + && self.calibration_hash == "0".repeat(64) + { + return Err(FusionError::EvidenceMismatch); + } + Ok(()) + } + + pub(crate) fn log_likelihood(&self, point: Vec3) -> f32 { + let d = point.minus(self.mean_m); + let mahalanobis = d.x * d.x / self.covariance_diagonal_m2.x + + d.y * d.y / self.covariance_diagonal_m2.y + + d.z * d.z / self.covariance_diagonal_m2.z; + -0.5 * mahalanobis * self.confidence + } +} + +fn valid_label(value: &str) -> bool { + !value.is_empty() + && value.len() <= 64 + && value + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b':')) +} + +fn valid_hash(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} + +/// CSI fusion boundary failure. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum FusionError { + /// A field was non-finite, out of bounds, or malformed. + #[error("invalid CSI spatial prior")] + InvalidPrior, + /// Synthetic evidence carried a non-synthetic calibration identity. + #[error("CSI source and evidence labels disagree")] + EvidenceMismatch, + /// The two modalities do not share the exact authorized join scope. + #[error("CSI and optical fusion bindings do not match")] + BindingMismatch, + /// Measured fusion is blocked until the output contract retains both + /// modality lineages and the deployment supplies authenticated bindings. + #[error("measured CSI fusion is unavailable in the v1 evidence contract")] + MeasuredFusionUnavailable, + /// A fusion binding is malformed or missing required identity. + #[error("invalid fusion scope binding")] + InvalidBinding, + /// A CSI observation was repeated or moved backwards. + #[error("replayed or out-of-order CSI prior")] + ReplayOrOutOfOrder, + /// Prior and optical frame are too far apart in time. + #[error("CSI prior is stale")] + Stale, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn calibrated_prior_prefers_nearby_particle() { + let p = CsiSpatialPrior { + source: FrameSource::Live, + sequence: 1, + captured_at_unix_ms: 1, + scope: FusionScope { + tenant_id: "tenant-1".into(), + workspace_id: "workspace-1".into(), + site_id: "site-1".into(), + world_frame_id: "world-1".into(), + session_id: "session-1".into(), + coordinate_transform_hash: "b".repeat(64), + }, + mean_m: Vec3::new(0.0, 0.0, 1.0), + covariance_diagonal_m2: Vec3::new(0.04, 0.04, 0.09), + confidence: 0.8, + evidence_level: EvidenceLevel::L2Calibrated, + sensor_id: "csi-1".into(), + calibration_hash: "a".repeat(64), + }; + p.validate().unwrap(); + assert!( + p.log_likelihood(Vec3::new(0.01, 0.0, 1.0)) + > p.log_likelihood(Vec3::new(1.0, 0.0, 1.0)) + ); + } + + #[test] + fn prior_rejects_synthetic_evidence_on_a_live_source() { + let mut p = CsiSpatialPrior { + source: FrameSource::Live, + sequence: 1, + captured_at_unix_ms: 1, + scope: FusionScope { + tenant_id: "tenant-1".into(), + workspace_id: "workspace-1".into(), + site_id: "site-1".into(), + world_frame_id: "world-1".into(), + session_id: "session-1".into(), + coordinate_transform_hash: "b".repeat(64), + }, + mean_m: Vec3::new(0.0, 0.0, 1.0), + covariance_diagonal_m2: Vec3::new(0.04, 0.04, 0.09), + confidence: 0.8, + evidence_level: EvidenceLevel::L0Synthetic, + sensor_id: "csi-1".into(), + calibration_hash: "0".repeat(64), + }; + assert_eq!(p.validate(), Err(FusionError::EvidenceMismatch)); + p.source = FrameSource::Synthetic; + assert!(p.validate().is_ok()); + } +} diff --git a/v2/crates/ruview-nlos/src/ingest.rs b/v2/crates/ruview-nlos/src/ingest.rs new file mode 100644 index 00000000..2e66c667 --- /dev/null +++ b/v2/crates/ruview-nlos/src/ingest.rs @@ -0,0 +1,397 @@ +//! Bounded decoder for the public VL53L8CH STM32 ASCII stream. +//! +//! The upstream firmware emits one row per zone: +//! `zone ambient distance_mm bin0 ... binN`. This decoder accumulates exactly +//! one unique row per zone and never mixes a malformed partial frame into the +//! next frame. + +use std::collections::BTreeMap; + +use thiserror::Error; + +use crate::protocol::{ + EvidenceLevel, FrameSource, Provenance, SensorPose, TransientFrame, TransientZone, Vec3, +}; +use crate::{MAX_BINS, MAX_ZONES, TRANSIENT_SCHEMA_V1}; + +/// Static configuration sent to and expected from the ST firmware. +#[derive(Clone, Debug)] +pub struct StDecoderConfig { + /// Capture session identifier. + pub session_id: String, + /// Grid height. + pub height: u16, + /// Grid width. + pub width: u16, + /// Histogram bins per zone. + pub num_bins: u16, + /// First firmware bin retained. + pub start_bin: u16, + /// Timing resolution in picoseconds. + pub bin_width_ps: f32, + /// Horizontal field of view in degrees. + pub fov_x_degrees: f32, + /// Vertical field of view in degrees. + pub fov_y_degrees: f32, + /// Add the reported ambient value back to every compact-normalized bin. + pub add_back_ambient: bool, + /// Require the upstream firmware's `D` frame-start marker. + pub require_frame_marker: bool, + /// Input source. + pub source: FrameSource, + /// Evidence level attached at ingest. + pub evidence_level: EvidenceLevel, + /// SHA-256 calibration digest or 64 zeroes while calibrating. + pub calibration_hash: String, + /// Sensor provenance. + pub provenance: Provenance, +} + +impl StDecoderConfig { + fn validate(&self) -> Result<(), DecodeError> { + let zones = usize::from(self.height) * usize::from(self.width); + if self.height == 0 + || self.width == 0 + || zones > MAX_ZONES + || !(8..=MAX_BINS).contains(&usize::from(self.num_bins)) + { + return Err(DecodeError::InvalidConfiguration); + } + if !self.bin_width_ps.is_finite() + || self.bin_width_ps <= 0.0 + || !self.fov_x_degrees.is_finite() + || !self.fov_y_degrees.is_finite() + || !(1.0..=120.0).contains(&self.fov_x_degrees) + || !(1.0..=120.0).contains(&self.fov_y_degrees) + { + return Err(DecodeError::InvalidConfiguration); + } + Ok(()) + } +} + +/// Incremental decoder for a trusted local serial device. +#[derive(Debug)] +pub struct StAsciiDecoder { + config: StDecoderConfig, + rows: BTreeMap, + next_sequence: u64, + began_frame: bool, +} + +#[derive(Debug)] +struct ParsedRow { + ambient: u32, + distance_m: f32, + histogram: Vec, +} + +impl StAsciiDecoder { + /// Create a decoder after checking all configured dimensions. + pub fn new(config: StDecoderConfig) -> Result { + config.validate()?; + Ok(Self { + config, + rows: BTreeMap::new(), + next_sequence: 0, + began_frame: false, + }) + } + + /// Consume one firmware line and return a complete frame when all zones arrive. + /// A malformed or duplicate row clears the partial frame before returning an + /// error, preventing cross-frame row splicing. + pub fn push_line( + &mut self, + line: &str, + captured_at_unix_ms: u64, + monotonic_ns: u64, + sensor_pose: SensorPose, + ) -> Result, DecodeError> { + if line.trim() == "D" { + self.rows.clear(); + self.began_frame = true; + return Ok(None); + } + if self.config.require_frame_marker && !self.began_frame { + // Firmware boot messages and stale rows before the next marker are + // deliberately ignored rather than incorporated into a frame. + return Ok(None); + } + if line.len() > 4_096 || line.bytes().any(|b| b == 0) { + self.rows.clear(); + self.began_frame = false; + return Err(DecodeError::MalformedRow); + } + let fields: Vec<&str> = line.split_ascii_whitespace().collect(); + if fields.len() != usize::from(self.config.num_bins) + 3 { + self.rows.clear(); + self.began_frame = false; + return Err(DecodeError::MalformedRow); + } + macro_rules! parse_or_reset { + ($raw:expr, $kind:ty) => { + match parse::<$kind>($raw) { + Ok(value) => value, + Err(error) => { + self.rows.clear(); + self.began_frame = false; + return Err(error); + } + } + }; + } + let zone_id = parse_or_reset!(fields[0], u16); + let zone_count = self.config.height * self.config.width; + if zone_id >= zone_count || self.rows.contains_key(&zone_id) { + self.rows.clear(); + self.began_frame = false; + return Err(DecodeError::InvalidZone); + } + let ambient = parse_or_reset!(fields[1], u32); + let distance_mm = parse_or_reset!(fields[2], f32); + if !distance_mm.is_finite() || !(10.0..=10_000.0).contains(&distance_mm) { + self.rows.clear(); + self.began_frame = false; + return Err(DecodeError::InvalidDistance); + } + let mut histogram = Vec::with_capacity(usize::from(self.config.num_bins)); + for raw in &fields[3..] { + let mut count = parse_or_reset!(raw, i64); + if self.config.add_back_ambient { + let Some(combined) = count.checked_add(i64::from(ambient)) else { + self.rows.clear(); + self.began_frame = false; + return Err(DecodeError::HistogramOverflow); + }; + count = combined; + } + if count > i64::from(u16::MAX) { + self.rows.clear(); + self.began_frame = false; + return Err(DecodeError::HistogramOverflow); + } + // The pinned public driver clips negative compact-normalized bins + // to zero. Positive overflow is rejected rather than saturated. + histogram.push(count.max(0) as u16); + } + self.rows.insert( + zone_id, + ParsedRow { + ambient, + distance_m: distance_mm / 1_000.0, + histogram, + }, + ); + if self.rows.len() != usize::from(zone_count) { + return Ok(None); + } + + let rows = std::mem::take(&mut self.rows); + self.began_frame = false; + let mut zones = Vec::with_capacity(rows.len()); + for (zone_id, row) in rows { + zones.push(TransientZone { + zone_id, + wall_point_m: point_from_zone( + zone_id, + row.distance_m, + self.config.height, + self.config.width, + self.config.fov_x_degrees, + self.config.fov_y_degrees, + ), + distance_m: row.distance_m, + ambient: row.ambient, + histogram: row.histogram, + }); + } + let frame = TransientFrame { + schema: TRANSIENT_SCHEMA_V1.into(), + session_id: self.config.session_id.clone(), + sequence: self.next_sequence, + captured_at_unix_ms, + monotonic_ns, + source: self.config.source, + evidence_level: self.config.evidence_level, + bin_width_ps: self.config.bin_width_ps, + start_bin: self.config.start_bin, + sensor_pose, + calibration_hash: self.config.calibration_hash.clone(), + provenance: self.config.provenance.clone(), + zones, + }; + frame.validate().map_err(DecodeError::Contract)?; + self.next_sequence += 1; + Ok(Some(frame)) + } + + /// Encode the 13 little-endian `uint16` values expected by the upstream + /// STM32 firmware. Network or serial I/O remains outside this pure function. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn firmware_config_bytes( + &self, + ranging_mode: u16, + ranging_frequency_hz: u16, + integration_time_ms: u16, + subsample: u16, + ) -> [u8; 26] { + let values = [ + self.config.height * self.config.width, + ranging_mode, + ranging_frequency_hz, + integration_time_ms, + self.config.start_bin, + self.config.num_bins, + subsample, + 0, + 0, + 1, + 1, + self.config.width, + self.config.height, + ]; + let mut bytes = [0_u8; 26]; + for (index, value) in values.iter().enumerate() { + bytes[index * 2..index * 2 + 2].copy_from_slice(&value.to_le_bytes()); + } + bytes + } +} + +fn parse(raw: &str) -> Result { + raw.parse().map_err(|_| DecodeError::MalformedRow) +} + +fn point_from_zone( + zone_id: u16, + distance_m: f32, + height: u16, + width: u16, + fov_x_degrees: f32, + fov_y_degrees: f32, +) -> Vec3 { + let row = f32::from(zone_id / width) + 0.5; + let col = f32::from(zone_id % width) + 0.5; + let yaw = (col / f32::from(width) - 0.5) * fov_x_degrees.to_radians(); + let pitch = (row / f32::from(height) - 0.5) * fov_y_degrees.to_radians(); + let cp = pitch.cos(); + Vec3::new( + distance_m * cp * yaw.sin(), + distance_m * pitch.sin(), + distance_m * cp * yaw.cos(), + ) +} + +/// Serial framing failure. +#[derive(Debug, Error)] +pub enum DecodeError { + /// Static configuration exceeds the supported sensor bounds. + #[error("invalid ST decoder configuration")] + InvalidConfiguration, + /// A row had the wrong length or a non-numeric token. + #[error("malformed ST histogram row")] + MalformedRow, + /// Zone id was out of range or duplicated. + #[error("invalid or duplicate ST zone")] + InvalidZone, + /// Direct wall distance was outside 1 cm to 10 m. + #[error("invalid ST wall distance")] + InvalidDistance, + /// A normalized histogram value could not be represented losslessly. + #[error("ST histogram value exceeds the u16 contract")] + HistogramOverflow, + /// The assembled frame violated the shared contract. + #[error(transparent)] + Contract(#[from] crate::protocol::ContractError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::{Provenance, TransientKind}; + + fn decoder() -> StAsciiDecoder { + StAsciiDecoder::new(StDecoderConfig { + session_id: "session-1".into(), + height: 2, + width: 2, + num_bins: 8, + start_bin: 30, + bin_width_ps: 250.0, + fov_x_degrees: 45.0, + fov_y_degrees: 45.0, + add_back_ambient: false, + require_frame_marker: true, + source: FrameSource::Live, + evidence_level: EvidenceLevel::L1Measured, + calibration_hash: "0".repeat(64), + provenance: Provenance { + sensor_id: "st-01".into(), + sensor_model: "VL53L8CH".into(), + firmware_version: "test".into(), + transient_kind: TransientKind::CompactNormalizedHistogram, + histogram_preserved: true, + transport: "usb_serial".into(), + }, + }) + .unwrap() + } + + #[test] + fn decodes_one_complete_frame_without_flattening_histograms() { + let mut d = decoder(); + d.push_line("D", 100, 200, SensorPose::default()).unwrap(); + for zone in 0..4 { + let result = d + .push_line( + &format!("{zone} 3 800 0 1 2 3 4 5 6 7"), + 100, + 200, + SensorPose::default(), + ) + .unwrap(); + if zone < 3 { + assert!(result.is_none()); + } else { + let frame = result.unwrap(); + assert_eq!(frame.zones.len(), 4); + assert_eq!(frame.zones[0].histogram, vec![0, 1, 2, 3, 4, 5, 6, 7]); + assert_eq!(frame.sequence, 0); + } + } + } + + #[test] + fn malformed_row_resets_partial_frame() { + let mut d = decoder(); + d.push_line("D", 1, 1, SensorPose::default()).unwrap(); + d.push_line("0 3 800 0 1 2 3 4 5 6 7", 1, 1, SensorPose::default()) + .unwrap(); + assert!(d.push_line("bad", 1, 1, SensorPose::default()).is_err()); + d.push_line("D", 1, 1, SensorPose::default()).unwrap(); + assert!(d + .push_line("1 3 800 0 1 2 3 4 5 6 7", 1, 1, SensorPose::default(),) + .unwrap() + .is_none()); + } + + #[test] + fn positive_histogram_overflow_is_rejected_not_saturated() { + let mut d = decoder(); + d.push_line("D", 1, 1, SensorPose::default()).unwrap(); + assert!(matches!( + d.push_line("0 3 800 70000 1 2 3 4 5 6 7", 1, 1, SensorPose::default()), + Err(DecodeError::HistogramOverflow) + )); + } + + #[test] + fn firmware_packet_matches_upstream_little_endian_layout() { + let bytes = decoder().firmware_config_bytes(1, 30, 10, 1); + assert_eq!(&bytes[0..2], &4_u16.to_le_bytes()); + assert_eq!(&bytes[8..10], &30_u16.to_le_bytes()); + assert_eq!(&bytes[24..26], &2_u16.to_le_bytes()); + } +} diff --git a/v2/crates/ruview-nlos/src/lib.rs b/v2/crates/ruview-nlos/src/lib.rs new file mode 100644 index 00000000..c5c0b7e3 --- /dev/null +++ b/v2/crates/ruview-nlos/src/lib.rs @@ -0,0 +1,45 @@ +//! Consumer time-of-flight non-line-of-sight sensing for RuView. +//! +//! This crate preserves the zone-level photon timing histograms needed by +//! motion-induced aperture sampling. It deliberately rejects ordinary depth +//! maps as live NLOS evidence. The deterministic simulator and its benchmarks +//! are evidence level L0 (synthetic); only a captured, witnessed hardware run +//! can satisfy the ADR-331 reproduction gate. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod bridge; +pub mod calibration; +pub mod fusion; +pub mod ingest; +pub mod protocol; +pub mod simulator; +pub mod tracker; + +#[cfg(feature = "server")] +pub mod server; + +pub use bridge::{RuFieldObservation, TemporalFeatureMemory, WorldGraphUpdate}; +pub use calibration::{Calibration, CalibrationConfig, PreprocessedFrame}; +pub use fusion::{CsiSpatialPrior, FusionScope}; +pub use ingest::{StAsciiDecoder, StDecoderConfig}; +pub use protocol::{ + EvidenceLevel, FrameSource, NlosTrack, Provenance, SensorPose, TrackEnvelope, TrackState, + TransientFrame, TransientKind, TransientZone, Vec3, +}; +pub use simulator::{BenchmarkReport, SyntheticScene}; +pub use tracker::{CanonicalObject, MotionApertureTracker, TrackerConfig}; + +/// Transient input schema identifier. +pub const TRANSIENT_SCHEMA_V1: &str = "ruview.nlos.transient.v1"; +/// Track output schema identifier shared by Rust, Swift, and TypeScript. +pub const TRACK_SCHEMA_V1: &str = "ruview.nlos.track.v1"; +/// Maximum JSON frame accepted by network clients. +pub const MAX_WIRE_BYTES: usize = 256 * 1024; +/// Maximum histogram zones accepted from one consumer sensor. +pub const MAX_ZONES: usize = 64; +/// Maximum temporal bins accepted per zone. +pub const MAX_BINS: usize = 128; +/// Maximum simultaneous hidden tracks on the public contract. +pub const MAX_TRACKS: usize = 16; diff --git a/v2/crates/ruview-nlos/src/protocol.rs b/v2/crates/ruview-nlos/src/protocol.rs new file mode 100644 index 00000000..b6cb2c75 --- /dev/null +++ b/v2/crates/ruview-nlos/src/protocol.rs @@ -0,0 +1,688 @@ +//! Bounded transient and track wire contracts (ADR-328, ADR-331). + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; +use thiserror::Error; + +use crate::{MAX_BINS, MAX_TRACKS, MAX_ZONES, TRACK_SCHEMA_V1, TRANSIENT_SCHEMA_V1}; + +const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; +const MAX_POSITION_M: f32 = 100.0; +const MAX_VELOCITY_MPS: f32 = 20.0; +const MAX_COVARIANCE_M2: f32 = 10.0; +const MAX_EXPIRY_WINDOW_MS: u64 = 5_000; + +/// A finite Cartesian vector in metres or metres per second. +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Vec3 { + /// X component. + pub x: f32, + /// Y component. + pub y: f32, + /// Z component. + pub z: f32, +} + +impl Vec3 { + /// Construct a vector. + #[must_use] + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } + + /// Euclidean distance. + #[must_use] + pub fn distance(self, other: Self) -> f32 { + let d = self.minus(other); + (d.x * d.x + d.y * d.y + d.z * d.z).sqrt() + } + + /// Component-wise addition. + #[must_use] + pub fn plus(self, other: Self) -> Self { + Self::new(self.x + other.x, self.y + other.y, self.z + other.z) + } + + /// Component-wise subtraction. + #[must_use] + pub fn minus(self, other: Self) -> Self { + Self::new(self.x - other.x, self.y - other.y, self.z - other.z) + } + + /// Scalar multiplication. + #[must_use] + pub fn scale(self, value: f32) -> Self { + Self::new(self.x * value, self.y * value, self.z * value) + } + + pub(crate) fn finite(self) -> bool { + self.x.is_finite() && self.y.is_finite() && self.z.is_finite() + } +} + +/// Unit quaternion plus translation describing a sensor pose. +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SensorPose { + /// Sensor origin in world coordinates. + pub translation_m: Vec3, + /// Quaternion ordered x, y, z, w. + pub quaternion_xyzw: [f32; 4], +} + +impl Default for SensorPose { + fn default() -> Self { + Self { + translation_m: Vec3::default(), + quaternion_xyzw: [0.0, 0.0, 0.0, 1.0], + } + } +} + +impl SensorPose { + /// Transform a point from sensor-local to world coordinates. + #[must_use] + pub fn transform(self, point: Vec3) -> Vec3 { + let [qx, qy, qz, qw] = self.quaternion_xyzw; + let q = Vec3::new(qx, qy, qz); + let t = cross(q, point).scale(2.0); + point + .plus(t.scale(qw)) + .plus(cross(q, t)) + .plus(self.translation_m) + } + + /// Validate finite translation and an approximately unit quaternion. + pub fn validate(self) -> Result<(), ContractError> { + if !self.translation_m.finite() + || self + .quaternion_xyzw + .iter() + .any(|component| !component.is_finite()) + { + return Err(ContractError::NonFinite("sensorPose")); + } + let norm = self + .quaternion_xyzw + .iter() + .map(|v| v * v) + .sum::() + .sqrt(); + if !(0.99..=1.01).contains(&norm) { + return Err(ContractError::InvalidPose); + } + Ok(()) + } +} + +fn cross(a: Vec3, b: Vec3) -> Vec3 { + Vec3::new( + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x, + ) +} + +/// Whether a frame came from a live sensor, captured replay, or generator. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FrameSource { + /// Authenticated live sensor input. + Live, + /// Immutable captured data replay. + Replay, + /// Deterministic generated data; never hardware evidence. + Synthetic, +} + +/// Evidence ladder shared by all NLOS outputs. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum EvidenceLevel { + /// Generated input only. + L0Synthetic, + /// A measured raw sensor frame with unverified calibration. + L1Measured, + /// Measured input bound to a valid calibration. + L2Calibrated, + /// Independently corroborated modality lineages. Ground-truth maturity is + /// evaluated separately and is never implied by this wire label. + L3Corroborated, +} + +/// Exact optical signal exposed by the adapter. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TransientKind { + /// Raw photon arrival histogram. + RawHistogram, + /// VL53L8CH compact normalized histogram. + CompactNormalizedHistogram, + /// Conventional depth map; insufficient for NLOS inversion. + DepthOnly, + /// Replayed raw or normalized histogram. + Replay, +} + +/// Transport provenance for a transient or track. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Provenance { + /// Authenticated or locally bound sensor identifier. + pub sensor_id: String, + /// Sensor model, for example `VL53L8CH`. + pub sensor_model: String, + /// Sensor firmware revision. + pub firmware_version: String, + /// Signal kind retained by the adapter. + pub transient_kind: TransientKind, + /// True only when zone timing bins remain available end to end. + pub histogram_preserved: bool, + /// `usb_serial`, `ruview_server`, or `replay`. + pub transport: String, +} + +impl Provenance { + fn validate(&self, source: FrameSource) -> Result<(), ContractError> { + validate_label("sensorId", &self.sensor_id)?; + validate_label("sensorModel", &self.sensor_model)?; + validate_label("firmwareVersion", &self.firmware_version)?; + if !matches!( + self.transport.as_str(), + "usb_serial" | "ruview_server" | "replay" + ) { + return Err(ContractError::InvalidValue("provenance.transport")); + } + if source == FrameSource::Live + && (!self.histogram_preserved + || matches!( + self.transient_kind, + TransientKind::DepthOnly | TransientKind::Replay + ) + || self.transport == "replay") + { + return Err(ContractError::DepthIsNotNlos); + } + if source == FrameSource::Synthetic + && (self.transport != "replay" || self.transient_kind != TransientKind::Replay) + { + return Err(ContractError::EvidenceMismatch); + } + if source == FrameSource::Replay + && (self.transport != "replay" + || self.transient_kind != TransientKind::Replay + || !self.histogram_preserved) + { + return Err(ContractError::EvidenceMismatch); + } + Ok(()) + } +} + +/// One SPAD zone with its uncollapsed timing histogram. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TransientZone { + /// Stable zone index within the frame. + pub zone_id: u16, + /// Relay-wall sample in sensor-local coordinates. + pub wall_point_m: Vec3, + /// Direct sensor-to-wall distance. + pub distance_m: f32, + /// Ambient counts reported by the sensor. + pub ambient: u32, + /// Photon counts by arrival-time bin. + pub histogram: Vec, +} + +/// Raw optical transient frame retained before NLOS processing. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TransientFrame { + /// Must equal [`TRANSIENT_SCHEMA_V1`]. + pub schema: String, + /// Capture session identity. + pub session_id: String, + /// Monotonic sequence within the session. + pub sequence: u64, + /// UTC capture time. + pub captured_at_unix_ms: u64, + /// Monotonic device time, used for aperture ordering. + pub monotonic_ns: u64, + /// Live, replay, or synthetic source. + pub source: FrameSource, + /// Evidence level attached at the ingest boundary. + pub evidence_level: EvidenceLevel, + /// Timing-bin width in picoseconds. + pub bin_width_ps: f32, + /// First physical sensor bin retained by the firmware. + pub start_bin: u16, + /// Sensor pose for motion-induced aperture accumulation. + pub sensor_pose: SensorPose, + /// Calibration digest, or 64 zeroes before calibration. + pub calibration_hash: String, + /// Sensor and transport provenance. + pub provenance: Provenance, + /// Zone histograms. + pub zones: Vec, +} + +impl TransientFrame { + /// Validate all untrusted dimensions, numbers, labels, and evidence rules. + pub fn validate(&self) -> Result<(), ContractError> { + if self.schema != TRANSIENT_SCHEMA_V1 { + return Err(ContractError::WrongSchema); + } + validate_label("sessionId", &self.session_id)?; + validate_sequence(self.sequence)?; + validate_timestamp(self.captured_at_unix_ms)?; + validate_hash(&self.calibration_hash)?; + self.sensor_pose.validate()?; + self.provenance.validate(self.source)?; + if !self.bin_width_ps.is_finite() || !(1.0..=10_000.0).contains(&self.bin_width_ps) { + return Err(ContractError::InvalidValue("binWidthPs")); + } + if self.zones.is_empty() || self.zones.len() > MAX_ZONES { + return Err(ContractError::Bound("zones")); + } + let bins = self.zones[0].histogram.len(); + if !(8..=MAX_BINS).contains(&bins) { + return Err(ContractError::Bound("histogram")); + } + let mut seen = [false; MAX_ZONES]; + for (zone_index, zone) in self.zones.iter().enumerate() { + let idx = usize::from(zone.zone_id); + if idx >= self.zones.len() || seen[idx] || idx != zone_index { + return Err(ContractError::InvalidValue("zoneId")); + } + seen[idx] = true; + if zone.histogram.len() != bins { + return Err(ContractError::InconsistentBins); + } + if !zone.wall_point_m.finite() + || !zone.distance_m.is_finite() + || !(0.01..=10.0).contains(&zone.distance_m) + { + return Err(ContractError::InvalidValue("zone geometry")); + } + } + match (self.source, self.evidence_level) { + (FrameSource::Synthetic, EvidenceLevel::L0Synthetic) => {} + (FrameSource::Synthetic, _) => return Err(ContractError::EvidenceMismatch), + (FrameSource::Live, EvidenceLevel::L0Synthetic) => { + return Err(ContractError::EvidenceMismatch) + } + _ => {} + } + if self.evidence_level == EvidenceLevel::L3Corroborated { + // v1 has no field that can retain both modality lineages. + return Err(ContractError::EvidenceMismatch); + } + if self.source == FrameSource::Synthetic && self.calibration_hash != "0".repeat(64) { + return Err(ContractError::EvidenceMismatch); + } + if self.evidence_level >= EvidenceLevel::L2Calibrated + && self.calibration_hash == "0".repeat(64) + { + return Err(ContractError::EvidenceMismatch); + } + Ok(()) + } +} + +/// State of one hidden target hypothesis. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TrackState { + /// Posterior passed all quality gates. + Tracking, + /// Some evidence remains, but quality is below the normal threshold. + Degraded, + /// No reliable estimate is available. + Unknown, +} + +/// Relative optical and RF contribution to one posterior. +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ModalityContributions { + /// Optical transient contribution. + pub lidar: f32, + /// CSI prior contribution. + pub csi: f32, +} + +/// One hidden target posterior. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NlosTrack { + /// Privacy-preserving session-local identifier. + pub track_id: String, + /// Quality-gated state. + pub state: TrackState, + /// Posterior mean in metres. + pub position_m: Vec3, + /// Estimated velocity. + pub velocity_mps: Vec3, + /// Diagonal covariance in square metres. + pub covariance_diagonal_m2: Vec3, + /// Bounded posterior confidence. + pub confidence: f32, + /// Shannon entropy of normalized particle weights. + pub posterior_entropy: f32, + /// Bounded optical/RF signal quality. + pub signal_quality: f32, + /// Relative modality contributions. + pub modality_contributions: ModalityContributions, +} + +/// Public track envelope shared with Swift and TypeScript clients. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TrackEnvelope { + /// Must equal [`TRACK_SCHEMA_V1`]. + pub schema: String, + /// Capture session identity. + pub session_id: String, + /// Monotonic sequence within the session. + pub sequence: u64, + /// UTC capture time. + pub captured_at_unix_ms: u64, + /// Hard expiry; clients fail closed after this time. + pub expires_at_unix_ms: u64, + /// Source label. + pub source: FrameSource, + /// Evidence level. + pub evidence_level: EvidenceLevel, + /// Reproducible algorithm revision. + pub algorithm_version: String, + /// Calibration SHA-256 digest. + pub calibration_hash: String, + /// Sensor and transport provenance. + pub provenance: Provenance, + /// Bounded hidden target hypotheses. + pub tracks: Vec, +} + +impl TrackEnvelope { + /// Validate a track frame received from an untrusted server or replay. + pub fn validate(&self) -> Result<(), ContractError> { + if self.schema != TRACK_SCHEMA_V1 { + return Err(ContractError::WrongSchema); + } + validate_label("sessionId", &self.session_id)?; + validate_label("algorithmVersion", &self.algorithm_version)?; + validate_sequence(self.sequence)?; + validate_timestamp(self.captured_at_unix_ms)?; + validate_timestamp(self.expires_at_unix_ms)?; + validate_hash(&self.calibration_hash)?; + self.provenance.validate(self.source)?; + if self.expires_at_unix_ms <= self.captured_at_unix_ms + || self.expires_at_unix_ms - self.captured_at_unix_ms > MAX_EXPIRY_WINDOW_MS + { + return Err(ContractError::InvalidExpiry); + } + if self.tracks.len() > MAX_TRACKS { + return Err(ContractError::Bound("tracks")); + } + if self.source == FrameSource::Synthetic + && self.evidence_level != EvidenceLevel::L0Synthetic + { + return Err(ContractError::EvidenceMismatch); + } + if self.source == FrameSource::Synthetic && self.calibration_hash != "0".repeat(64) { + return Err(ContractError::EvidenceMismatch); + } + if self.evidence_level >= EvidenceLevel::L2Calibrated + && self.calibration_hash == "0".repeat(64) + { + return Err(ContractError::EvidenceMismatch); + } + if self.source == FrameSource::Live && self.evidence_level == EvidenceLevel::L0Synthetic { + return Err(ContractError::EvidenceMismatch); + } + if self.evidence_level == EvidenceLevel::L3Corroborated { + return Err(ContractError::EvidenceMismatch); + } + let mut track_ids = BTreeSet::new(); + for track in &self.tracks { + validate_label("trackId", &track.track_id)?; + if !track_ids.insert(track.track_id.as_str()) { + return Err(ContractError::InvalidValue("duplicate trackId")); + } + validate_bounded_vec(track.position_m, MAX_POSITION_M, "positionM")?; + validate_bounded_vec(track.velocity_mps, MAX_VELOCITY_MPS, "velocityMps")?; + validate_nonnegative_vec( + track.covariance_diagonal_m2, + MAX_COVARIANCE_M2, + "covarianceDiagonalM2", + )?; + validate_unit(track.confidence, "confidence")?; + validate_unit(track.signal_quality, "signalQuality")?; + validate_unit(track.modality_contributions.lidar, "lidar contribution")?; + validate_unit(track.modality_contributions.csi, "csi contribution")?; + let contribution_sum = + track.modality_contributions.lidar + track.modality_contributions.csi; + if !(0.999..=1.001).contains(&contribution_sum) { + return Err(ContractError::InvalidValue("modality contributions")); + } + if !track.posterior_entropy.is_finite() || track.posterior_entropy < 0.0 { + return Err(ContractError::NonFinite("posteriorEntropy")); + } + } + Ok(()) + } +} + +/// Contract validation failure. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +pub enum ContractError { + /// Schema identifier does not match this implementation. + #[error("unsupported NLOS schema")] + WrongSchema, + /// A collection exceeded its explicit bound. + #[error("{0} exceeds its allowed bound")] + Bound(&'static str), + /// A number was NaN or infinite. + #[error("{0} must be finite")] + NonFinite(&'static str), + /// A field value is invalid. + #[error("invalid {0}")] + InvalidValue(&'static str), + /// A label is empty, too long, or unsafe. + #[error("invalid bounded label {0}")] + InvalidLabel(&'static str), + /// Zone histograms do not share one bin count. + #[error("all zones must have the same histogram bin count")] + InconsistentBins, + /// Ordinary depth is not sufficient live NLOS evidence. + #[error("depth-only frames cannot be presented as live transient NLOS")] + DepthIsNotNlos, + /// Source and evidence labels disagree. + #[error("source and evidence level disagree")] + EvidenceMismatch, + /// A digest was not lowercase SHA-256 hex. + #[error("calibrationHash must be 64 lowercase hexadecimal characters")] + InvalidHash, + /// Sequence is not interoperable with JavaScript clients. + #[error("sequence exceeds the JavaScript safe integer range")] + UnsafeSequence, + /// Timestamp is not interoperable with JavaScript clients. + #[error("timestamp exceeds the JavaScript safe integer range")] + UnsafeTimestamp, + /// Expiry precedes capture or exceeds the five second freshness bound. + #[error("invalid frame expiry")] + InvalidExpiry, + /// Quaternion is not approximately unit length. + #[error("sensor pose quaternion must be normalized")] + InvalidPose, +} + +fn validate_label(field: &'static str, value: &str) -> Result<(), ContractError> { + if value.is_empty() + || value.len() > 64 + || value + .bytes() + .any(|b| !(b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b':'))) + { + return Err(ContractError::InvalidLabel(field)); + } + Ok(()) +} + +fn validate_hash(value: &str) -> Result<(), ContractError> { + if value.len() != 64 + || !value + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + { + return Err(ContractError::InvalidHash); + } + Ok(()) +} + +fn validate_sequence(value: u64) -> Result<(), ContractError> { + if value > MAX_SAFE_INTEGER { + return Err(ContractError::UnsafeSequence); + } + Ok(()) +} + +fn validate_timestamp(value: u64) -> Result<(), ContractError> { + if value > MAX_SAFE_INTEGER { + return Err(ContractError::UnsafeTimestamp); + } + Ok(()) +} + +fn validate_unit(value: f32, field: &'static str) -> Result<(), ContractError> { + if !value.is_finite() || !(0.0..=1.0).contains(&value) { + return Err(ContractError::InvalidValue(field)); + } + Ok(()) +} + +fn validate_bounded_vec( + value: Vec3, + max_abs: f32, + field: &'static str, +) -> Result<(), ContractError> { + if !value.finite() + || [value.x, value.y, value.z] + .iter() + .any(|v| v.abs() > max_abs) + { + return Err(ContractError::InvalidValue(field)); + } + Ok(()) +} + +fn validate_nonnegative_vec( + value: Vec3, + max: f32, + field: &'static str, +) -> Result<(), ContractError> { + if !value.finite() + || [value.x, value.y, value.z] + .iter() + .any(|v| *v < 0.0 || *v > max) + { + return Err(ContractError::InvalidValue(field)); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn provenance() -> Provenance { + Provenance { + sensor_id: "st-01".into(), + sensor_model: "VL53L8CH".into(), + firmware_version: "upstream-main".into(), + transient_kind: TransientKind::CompactNormalizedHistogram, + histogram_preserved: true, + transport: "usb_serial".into(), + } + } + + #[test] + fn depth_only_live_is_rejected() { + let mut p = provenance(); + p.transient_kind = TransientKind::DepthOnly; + p.histogram_preserved = false; + assert_eq!( + p.validate(FrameSource::Live), + Err(ContractError::DepthIsNotNlos) + ); + let mut replay_transport = provenance(); + replay_transport.transport = "replay".into(); + assert_eq!( + replay_transport.validate(FrameSource::Live), + Err(ContractError::DepthIsNotNlos) + ); + } + + #[test] + fn pose_transform_applies_rotation_and_translation() { + let half = (0.5_f32).sqrt(); + let pose = SensorPose { + translation_m: Vec3::new(1.0, 0.0, 0.0), + quaternion_xyzw: [0.0, 0.0, half, half], + }; + pose.validate().unwrap(); + let out = pose.transform(Vec3::new(1.0, 0.0, 0.0)); + assert!((out.x - 1.0).abs() < 1e-5); + assert!((out.y - 1.0).abs() < 1e-5); + } + + #[test] + fn track_contract_rejects_duplicate_ids_and_zero_lifetime() { + let fixture = include_str!("../tests/fixtures/track_synthetic.json"); + let mut envelope: TrackEnvelope = serde_json::from_str(fixture).unwrap(); + envelope.tracks.push(envelope.tracks[0].clone()); + assert_eq!( + envelope.validate(), + Err(ContractError::InvalidValue("duplicate trackId")) + ); + + envelope.tracks.truncate(1); + envelope.expires_at_unix_ms = envelope.captured_at_unix_ms; + assert_eq!(envelope.validate(), Err(ContractError::InvalidExpiry)); + } + + #[test] + fn v1_rejects_l3_without_dual_modality_lineage() { + let fixture = include_str!("../tests/fixtures/track_synthetic.json"); + let mut frame: TrackEnvelope = serde_json::from_str(fixture).unwrap(); + frame.source = FrameSource::Replay; + frame.evidence_level = EvidenceLevel::L3Corroborated; + frame.calibration_hash = "a".repeat(64); + assert_eq!(frame.validate(), Err(ContractError::EvidenceMismatch)); + } + + #[test] + fn transient_v1_rejects_l3_at_the_ingest_boundary() { + let mut scene = crate::simulator::SyntheticScene::default(); + let mut frame = scene.frame(None, 0.0, 1); + frame.source = FrameSource::Replay; + frame.evidence_level = EvidenceLevel::L3Corroborated; + frame.calibration_hash = "a".repeat(64); + assert_eq!(frame.validate(), Err(ContractError::EvidenceMismatch)); + } + + #[test] + fn captured_replay_must_preserve_replayed_histogram_provenance() { + let mut p = provenance(); + p.transport = "replay".into(); + p.transient_kind = TransientKind::Replay; + assert!(p.validate(FrameSource::Replay).is_ok()); + p.histogram_preserved = false; + assert_eq!( + p.validate(FrameSource::Replay), + Err(ContractError::EvidenceMismatch) + ); + } +} diff --git a/v2/crates/ruview-nlos/src/server.rs b/v2/crates/ruview-nlos/src/server.rs new file mode 100644 index 00000000..0df84cc4 --- /dev/null +++ b/v2/crates/ruview-nlos/src/server.rs @@ -0,0 +1,791 @@ +//! Authenticated read-only NLOS HTTP and WebSocket surface. +//! +//! Browsers exchange a bearer token for a single-use, short-lived WebSocket +//! ticket because the browser WebSocket API cannot set an Authorization header. +//! Native clients may authenticate the upgrade directly with a bearer header. + +use std::collections::{BTreeMap, VecDeque}; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; +use axum::extract::{Query, State}; +use axum::http::{HeaderMap, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use tokio::sync::{broadcast, RwLock}; +use tower_http::cors::CorsLayer; +use tower_http::limit::RequestBodyLimitLayer; + +use crate::protocol::TrackEnvelope; +use crate::{MAX_WIRE_BYTES, TRACK_SCHEMA_V1}; + +const TICKET_TTL_MS: u64 = 30_000; +const SESSION_TTL_MS: u64 = 60 * 60 * 1_000; +const MAX_TICKETS: usize = 1_024; +const MAX_HISTORY: usize = 64; + +/// Thread-safe authenticated publication hub. +#[derive(Clone)] +pub struct NlosHub { + inner: Arc, +} + +struct HubInner { + bearer_digest: [u8; 32], + session_id: String, + latest: RwLock>, + history: RwLock>, + publish_order: tokio::sync::Mutex<()>, + last_published_sequence: tokio::sync::Mutex>, + tickets: Mutex>, + broadcast: broadcast::Sender, + allowed_origin: Option, +} + +#[derive(Clone, Copy)] +struct Ticket { + expires_at_unix_ms: u64, + origin_digest: Option<[u8; 32]>, +} + +impl NlosHub { + /// Create a hub. The bearer token is hashed immediately and never retained. + pub fn new(bearer_token: &str, session_id: impl Into) -> Result { + if bearer_token.len() < 32 + || bearer_token.len() > 512 + || bearer_token + .bytes() + .any(|byte| !(0x21..=0x7e).contains(&byte)) + { + return Err(ServerError::WeakToken); + } + let session_id = session_id.into(); + if session_id.is_empty() + || session_id.len() > 64 + || session_id + .bytes() + .any(|b| !(b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b':'))) + { + return Err(ServerError::InvalidSession); + } + let (broadcast, _) = broadcast::channel(64); + Ok(Self { + inner: Arc::new(HubInner { + bearer_digest: sha256(bearer_token.as_bytes()), + session_id, + latest: RwLock::new(None), + history: RwLock::new(VecDeque::with_capacity(MAX_HISTORY)), + publish_order: tokio::sync::Mutex::new(()), + last_published_sequence: tokio::sync::Mutex::new(None), + tickets: Mutex::new(BTreeMap::new()), + broadcast, + allowed_origin: None, + }), + }) + } + + /// Permit one exact browser origin for ticket exchange. Wildcards, + /// credentials, paths, fragments, and cleartext non-loopback origins are + /// rejected. Call this before cloning the hub. + pub fn with_allowed_origin(mut self, origin: &str) -> Result { + let value = validate_origin(origin)?; + let Some(inner) = Arc::get_mut(&mut self.inner) else { + return Err(ServerError::Internal); + }; + inner.allowed_origin = Some(value); + Ok(self) + } + + /// Publish one validated, bounded, monotonically ordered envelope. + pub async fn publish(&self, envelope: TrackEnvelope) -> Result<(), ServerError> { + envelope.validate()?; + if envelope.session_id != self.inner.session_id { + return Err(ServerError::SessionMismatch); + } + let encoded = serde_json::to_vec(&envelope).map_err(|_| ServerError::Serialization)?; + if encoded.len() > MAX_WIRE_BYTES { + return Err(ServerError::FrameTooLarge); + } + let now = now_unix_ms(); + if envelope.expires_at_unix_ms <= now + || envelope.captured_at_unix_ms > now.saturating_add(1_000) + { + return Err(ServerError::StaleOrFuture); + } + // Serialize the latest/history/broadcast transition so concurrent + // publishers cannot expose sequence N+1 before N in another surface. + let _publish_order = self.inner.publish_order.lock().await; + let mut last_sequence = self.inner.last_published_sequence.lock().await; + if last_sequence.is_some_and(|previous| previous >= envelope.sequence) { + return Err(ServerError::ReplayOrOutOfOrder); + } + *last_sequence = Some(envelope.sequence); + drop(last_sequence); + let mut latest = self.inner.latest.write().await; + *latest = Some(envelope.clone()); + drop(latest); + let mut history = self.inner.history.write().await; + history.retain(|item| item.expires_at_unix_ms > now); + while history.len() >= MAX_HISTORY { + history.pop_front(); + } + history.push_back(envelope.clone()); + drop(history); + let _ = self.inner.broadcast.send(envelope); + Ok(()) + } + + async fn purge_expired(&self, now: u64) { + let _publish_order = self.inner.publish_order.lock().await; + let mut latest = self.inner.latest.write().await; + if latest + .as_ref() + .is_some_and(|item| item.expires_at_unix_ms <= now) + { + *latest = None; + } + drop(latest); + self.inner + .history + .write() + .await + .retain(|item| item.expires_at_unix_ms > now); + } + + /// Build the read-only API router. No permissive CORS layer is installed. + pub fn router(self) -> Router { + let router = Router::new() + .route("/health", get(health)) + .route("/api/v1/nlos/latest", get(latest)) + .route("/api/v1/nlos/tracks", get(tracks)) + .route("/api/v1/nlos/ws-ticket", post(issue_ws_ticket)) + .route("/api/v1/nlos/ws", get(websocket)) + .layer(RequestBodyLimitLayer::new(8 * 1024)) + .with_state(self.clone()); + if let Some(origin) = self.inner.allowed_origin.clone() { + router.layer( + CorsLayer::new() + .allow_origin(origin) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) + .allow_headers([ + axum::http::header::AUTHORIZATION, + axum::http::header::CONTENT_TYPE, + ]), + ) + } else { + // No CORS headers means browsers remain same-origin by default. + router + } + } + + fn bearer_authorized(&self, headers: &HeaderMap) -> bool { + let Some(value) = headers.get(axum::http::header::AUTHORIZATION) else { + return false; + }; + let Ok(value) = value.to_str() else { + return false; + }; + let Some(token) = value.strip_prefix("Bearer ") else { + return false; + }; + constant_time_eq(&sha256(token.as_bytes()), &self.inner.bearer_digest) + } + + fn issue_ticket( + &self, + now: u64, + origin_digest: Option<[u8; 32]>, + ) -> Result<(String, u64), ServerError> { + let mut tickets = self + .inner + .tickets + .lock() + .map_err(|_| ServerError::Internal)?; + tickets.retain(|_, ticket| ticket.expires_at_unix_ms > now); + if tickets.len() >= MAX_TICKETS { + return Err(ServerError::TicketCapacity); + } + let mut random = [0_u8; 32]; + getrandom::getrandom(&mut random).map_err(|_| ServerError::Entropy)?; + let ticket = lowercase_hex(&random); + let expires = now.saturating_add(TICKET_TTL_MS); + tickets.insert( + ticket.clone(), + Ticket { + expires_at_unix_ms: expires, + origin_digest, + }, + ); + Ok((ticket, expires)) + } + + fn consume_ticket(&self, raw: &str, now: u64, origin_digest: Option<[u8; 32]>) -> bool { + if raw.len() != 64 + || !raw + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + { + return false; + } + let Ok(mut tickets) = self.inner.tickets.lock() else { + return false; + }; + tickets.remove(raw).is_some_and(|ticket| { + ticket.expires_at_unix_ms >= now && ticket.origin_digest == origin_digest + }) + } + + fn request_origin_digest(&self, headers: &HeaderMap) -> Result, ServerError> { + let actual = headers.get(axum::http::header::ORIGIN); + if let Some(expected) = &self.inner.allowed_origin { + if actual != Some(expected) { + return Err(ServerError::InvalidOrigin); + } + } + Ok(actual.map(|value| sha256(value.as_bytes()))) + } +} + +async fn health() -> Json { + Json(Health { + status: "ok", + service: "ruview-nlos", + }) +} + +async fn latest(State(hub): State, headers: HeaderMap) -> Response { + if !hub.bearer_authorized(&headers) { + return unauthorized(); + } + hub.purge_expired(now_unix_ms()).await; + match hub.inner.latest.read().await.clone() { + Some(frame) => sensitive(Json(frame).into_response()), + None => sensitive(StatusCode::NO_CONTENT.into_response()), + } +} + +async fn tracks(State(hub): State, headers: HeaderMap) -> Response { + if !hub.bearer_authorized(&headers) { + return unauthorized(); + } + hub.purge_expired(now_unix_ms()).await; + let history: Vec<_> = hub.inner.history.read().await.iter().cloned().collect(); + sensitive(Json(history).into_response()) +} + +async fn issue_ws_ticket(State(hub): State, headers: HeaderMap) -> Response { + if !hub.bearer_authorized(&headers) { + return unauthorized(); + } + let now = now_unix_ms(); + let origin_digest = match hub.request_origin_digest(&headers) { + Ok(value) => value, + Err(_) => return sensitive(StatusCode::FORBIDDEN.into_response()), + }; + let (ticket, expires_at_unix_ms) = match hub.issue_ticket(now, origin_digest) { + Ok(result) => result, + Err(_) => return sensitive(StatusCode::SERVICE_UNAVAILABLE.into_response()), + }; + let host = headers + .get(axum::http::header::HOST) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()) + .map_or_else(|| "127.0.0.1:8787".to_owned(), |value| value.to_string()); + let forwarded = headers + .get("x-forwarded-proto") + .and_then(|value| value.to_str().ok()); + let scheme = if matches!(forwarded, Some("https" | "wss")) { + "wss" + } else { + "ws" + }; + sensitive( + Json(WsTicketResponse { + schema: "ruview.nlos.ws-ticket.v1", + web_socket_url: format!("{scheme}://{host}/api/v1/nlos/ws?ticket={ticket}"), + expires_at_unix_ms, + }) + .into_response(), + ) +} + +async fn websocket( + State(hub): State, + headers: HeaderMap, + Query(query): Query, + upgrade: WebSocketUpgrade, +) -> Response { + let now = now_unix_ms(); + let origin_digest = headers + .get(axum::http::header::ORIGIN) + .map(|value| sha256(value.as_bytes())); + let ticket_ok = query + .ticket + .as_deref() + .is_some_and(|ticket| hub.consume_ticket(ticket, now, origin_digest)); + if !ticket_ok && !hub.bearer_authorized(&headers) { + return unauthorized(); + } + upgrade + .protocols([TRACK_SCHEMA_V1]) + .max_message_size(8 * 1024) + .max_frame_size(8 * 1024) + .on_upgrade(move |socket| websocket_session(hub, socket)) +} + +async fn websocket_session(hub: NlosHub, mut socket: WebSocket) { + // Subscribe before reading the retained latest value. The sequence + // watermark below de-duplicates a publication that lands in between. + let mut receiver = hub.inner.broadcast.subscribe(); + let session_expires_at_unix_ms = now_unix_ms().saturating_add(SESSION_TTL_MS); + let authenticated = Authenticated { + schema: "ruview.nlos.authenticated.v1", + session_id: hub.inner.session_id.clone(), + expires_at_unix_ms: session_expires_at_unix_ms, + }; + let Ok(payload) = serde_json::to_string(&authenticated) else { + return; + }; + if socket.send(Message::Text(payload)).await.is_err() { + return; + } + let mut last_sent_sequence = None; + if let Some(latest) = hub + .inner + .latest + .read() + .await + .clone() + .filter(|frame| frame.expires_at_unix_ms > now_unix_ms()) + { + let Ok(payload) = serde_json::to_string(&latest) else { + return; + }; + if socket.send(Message::Text(payload)).await.is_err() { + return; + } + last_sent_sequence = Some(latest.sequence); + } + let session_expiry = tokio::time::sleep(std::time::Duration::from_millis(SESSION_TTL_MS)); + tokio::pin!(session_expiry); + loop { + tokio::select! { + message = receiver.recv() => { + let frame = match message { + Ok(frame) => frame, + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue, + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + }; + if last_sent_sequence.is_some_and(|last| frame.sequence <= last) { + continue; + } + let Ok(payload) = serde_json::to_string(&frame) else { break; }; + if socket.send(Message::Text(payload)).await.is_err() { break; } + last_sent_sequence = Some(frame.sequence); + } + incoming = socket.recv() => { + match incoming { + Some(Ok(Message::Close(_))) | None | Some(Err(_)) => break, + Some(Ok(Message::Ping(value))) => { + if socket.send(Message::Pong(value)).await.is_err() { break; } + } + _ => {} + } + } + _ = &mut session_expiry => { + let _ = socket.send(Message::Close(None)).await; + break; + } + } + } +} + +fn unauthorized() -> Response { + sensitive( + ( + StatusCode::UNAUTHORIZED, + Json(ApiError { + error: "unauthorized", + }), + ) + .into_response(), + ) +} + +fn sensitive(mut response: Response) -> Response { + response.headers_mut().insert( + axum::http::header::CACHE_CONTROL, + HeaderValue::from_static("no-store, max-age=0"), + ); + response +} + +fn now_unix_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |duration| duration.as_millis() as u64) +} + +fn sha256(value: &[u8]) -> [u8; 32] { + Sha256::digest(value).into() +} + +fn constant_time_eq(left: &[u8; 32], right: &[u8; 32]) -> bool { + left.iter() + .zip(right.iter()) + .fold(0_u8, |difference, (a, b)| difference | (a ^ b)) + == 0 +} + +fn lowercase_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn validate_origin(origin: &str) -> Result { + if origin.len() > 2_048 || origin.chars().any(|value| matches!(value, '#' | '?' | '@')) { + return Err(ServerError::InvalidOrigin); + } + let uri: axum::http::Uri = origin.parse().map_err(|_| ServerError::InvalidOrigin)?; + let scheme = uri.scheme_str().ok_or(ServerError::InvalidOrigin)?; + let authority = uri.authority().ok_or(ServerError::InvalidOrigin)?; + if uri + .path_and_query() + .is_some_and(|path| path.as_str() != "/") + { + return Err(ServerError::InvalidOrigin); + } + let host = authority.host(); + let loopback = matches!(host, "localhost" | "127.0.0.1" | "::1" | "[::1]"); + if scheme != "https" && !(scheme == "http" && loopback) { + return Err(ServerError::InvalidOrigin); + } + origin.parse().map_err(|_| ServerError::InvalidOrigin) +} + +#[derive(Serialize)] +struct Health { + status: &'static str, + service: &'static str, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct WsTicketResponse { + schema: &'static str, + web_socket_url: String, + expires_at_unix_ms: u64, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Authenticated { + schema: &'static str, + session_id: String, + expires_at_unix_ms: u64, +} + +#[derive(Deserialize)] +struct WsQuery { + ticket: Option, +} + +#[derive(Serialize)] +struct ApiError { + error: &'static str, +} + +/// Server security or publication failure. +#[derive(Debug, Error)] +pub enum ServerError { + /// Token is outside the shared visible-ASCII length boundary. + #[error("bearer token must contain 32 to 512 visible ASCII characters")] + WeakToken, + /// Session id is empty or too long. + #[error("invalid server session id")] + InvalidSession, + /// Track frame contract failure. + #[error(transparent)] + Contract(#[from] crate::protocol::ContractError), + /// Encoded frame exceeded 256 KiB. + #[error("track frame exceeds the wire-size bound")] + FrameTooLarge, + /// Sequence moved backwards or repeated. + #[error("replayed or out-of-order track frame")] + ReplayOrOutOfOrder, + /// Frame was already expired or materially future-dated at publication. + #[error("stale or future-dated track frame")] + StaleOrFuture, + /// Publisher session did not match the authenticated server session. + #[error("track frame session does not match server session")] + SessionMismatch, + /// Random ticket generation failed. + #[error("secure entropy unavailable")] + Entropy, + /// Too many unexpired tickets exist. + #[error("ticket capacity reached")] + TicketCapacity, + /// Lock poisoning or another internal failure. + #[error("internal server failure")] + Internal, + /// JSON serialization failed. + #[error("frame serialization failed")] + Serialization, + /// Browser origin was not one exact HTTPS origin or loopback development origin. + #[error("invalid allowed browser origin")] + InvalidOrigin, +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + use axum::http::Request; + use http_body_util::BodyExt; + use tower::ServiceExt; + + #[test] + fn ticket_is_single_use_and_expiring() { + let hub = NlosHub::new(&"x".repeat(32), "s1").unwrap(); + let origin = Some(sha256(b"https://app.example.test")); + let (ticket, expires) = hub.issue_ticket(1_000, origin).unwrap(); + assert!(!hub.consume_ticket(&ticket, expires, None)); + let (ticket, expires) = hub.issue_ticket(1_500, origin).unwrap(); + assert!(hub.consume_ticket(&ticket, expires, origin)); + assert!(!hub.consume_ticket(&ticket, expires, origin)); + let (ticket, expires) = hub.issue_ticket(2_000, origin).unwrap(); + assert!(!hub.consume_ticket(&ticket, expires + 1, origin)); + } + + #[test] + fn token_digest_comparison_is_exact() { + let hub = NlosHub::new(&"a".repeat(32), "s1").unwrap(); + let mut headers = HeaderMap::new(); + headers.insert( + axum::http::header::AUTHORIZATION, + format!("Bearer {}", "a".repeat(32)).parse().unwrap(), + ); + assert!(hub.bearer_authorized(&headers)); + headers.insert( + axum::http::header::AUTHORIZATION, + format!("Bearer {}", "b".repeat(32)).parse().unwrap(), + ); + assert!(!hub.bearer_authorized(&headers)); + } + + #[test] + fn browser_origin_is_exact_and_https_except_loopback() { + assert!(NlosHub::new(&"x".repeat(32), "s1") + .unwrap() + .with_allowed_origin("https://app.example.test") + .is_ok()); + assert!(NlosHub::new(&"x".repeat(32), "s1") + .unwrap() + .with_allowed_origin("http://127.0.0.1:8081") + .is_ok()); + for invalid in [ + "*", + "http://app.example.test", + "https://user@example.test", + "https://example.test/path", + ] { + assert!(NlosHub::new(&"x".repeat(32), "s1") + .unwrap() + .with_allowed_origin(invalid) + .is_err()); + } + } + + #[tokio::test] + async fn ticket_endpoint_requires_bearer_and_returns_strict_contract() { + let token = "z".repeat(32); + let router = NlosHub::new(&token, "s1").unwrap().router(); + let unauthorized = router + .clone() + .oneshot( + Request::post("/api/v1/nlos/ws-ticket") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED); + + let authorized = router + .oneshot( + Request::post("/api/v1/nlos/ws-ticket") + .header("host", "127.0.0.1:8787") + .header("authorization", format!("Bearer {token}")) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(authorized.status(), StatusCode::OK); + assert_eq!( + authorized.headers().get(axum::http::header::CACHE_CONTROL), + Some(&HeaderValue::from_static("no-store, max-age=0")) + ); + let bytes = authorized.into_body().collect().await.unwrap().to_bytes(); + assert!(bytes.len() < 8 * 1024); + let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(value["schema"], "ruview.nlos.ws-ticket.v1"); + assert!(value["webSocketUrl"] + .as_str() + .unwrap() + .starts_with("ws://127.0.0.1:8787/api/v1/nlos/ws?ticket=")); + assert!(value["expiresAtUnixMs"].as_u64().is_some()); + } + + #[tokio::test] + async fn exact_origin_preflight_is_allowed_without_wildcard() { + let router = NlosHub::new(&"x".repeat(32), "s1") + .unwrap() + .with_allowed_origin("https://app.example.test") + .unwrap() + .router(); + let response = router + .oneshot( + Request::builder() + .method("OPTIONS") + .uri("/api/v1/nlos/ws-ticket") + .header("origin", "https://app.example.test") + .header("access-control-request-method", "POST") + .header("access-control-request-headers", "authorization") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get("access-control-allow-origin") + .unwrap(), + "https://app.example.test" + ); + assert_ne!( + response + .headers() + .get("access-control-allow-origin") + .unwrap(), + "*" + ); + } + + #[tokio::test] + async fn configured_origin_is_required_for_ticket_issue() { + let token = "z".repeat(32); + let router = NlosHub::new(&token, "s1") + .unwrap() + .with_allowed_origin("https://app.example.test") + .unwrap() + .router(); + let forbidden = router + .clone() + .oneshot( + Request::post("/api/v1/nlos/ws-ticket") + .header("host", "nlos.example.test") + .header("authorization", format!("Bearer {token}")) + .header("origin", "https://other.example.test") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(forbidden.status(), StatusCode::FORBIDDEN); + assert_eq!( + forbidden.headers().get(axum::http::header::CACHE_CONTROL), + Some(&HeaderValue::from_static("no-store, max-age=0")) + ); + } + + #[tokio::test] + async fn publisher_rejects_session_switch_and_sequence_replay() { + let fixture = include_str!("../tests/fixtures/track_synthetic.json"); + let mut frame: TrackEnvelope = serde_json::from_str(fixture).unwrap(); + let now = now_unix_ms(); + frame.captured_at_unix_ms = now; + frame.expires_at_unix_ms = now + 250; + let hub = NlosHub::new(&"x".repeat(32), frame.session_id.clone()).unwrap(); + hub.publish(frame.clone()).await.unwrap(); + assert!(matches!( + hub.publish(frame.clone()).await, + Err(ServerError::ReplayOrOutOfOrder) + )); + let mut wrong_session = frame; + wrong_session.session_id = "another-session".into(); + wrong_session.sequence += 1; + assert!(matches!( + hub.publish(wrong_session).await, + Err(ServerError::SessionMismatch) + )); + } + + #[tokio::test] + async fn concurrent_publication_keeps_history_monotonic() { + let fixture = include_str!("../tests/fixtures/track_synthetic.json"); + let mut frame: TrackEnvelope = serde_json::from_str(fixture).unwrap(); + let now = now_unix_ms(); + frame.captured_at_unix_ms = now; + frame.expires_at_unix_ms = now + 250; + let hub = NlosHub::new(&"x".repeat(32), frame.session_id.clone()).unwrap(); + let barrier = Arc::new(tokio::sync::Barrier::new(32)); + let mut tasks = Vec::new(); + for sequence in 1..=32_u64 { + let hub = hub.clone(); + let barrier = barrier.clone(); + let mut candidate = frame.clone(); + candidate.sequence = sequence; + tasks.push(tokio::spawn(async move { + barrier.wait().await; + hub.publish(candidate).await.ok() + })); + } + for task in tasks { + task.await.unwrap(); + } + + let history = hub.inner.history.read().await; + let sequences: Vec<_> = history.iter().map(|item| item.sequence).collect(); + assert!(!sequences.is_empty()); + assert!(sequences.windows(2).all(|pair| pair[0] < pair[1])); + assert_eq!( + hub.inner + .latest + .read() + .await + .as_ref() + .map(|item| item.sequence), + sequences.last().copied() + ); + } + + #[tokio::test] + async fn expired_tracks_are_removed_from_all_retention_surfaces() { + let fixture = include_str!("../tests/fixtures/track_synthetic.json"); + let mut frame: TrackEnvelope = serde_json::from_str(fixture).unwrap(); + let now = now_unix_ms(); + frame.captured_at_unix_ms = now; + frame.expires_at_unix_ms = now + 5; + let hub = NlosHub::new(&"x".repeat(32), frame.session_id.clone()).unwrap(); + hub.publish(frame).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + // Public read surfaces call the same lazy purge before returning data; + // no unbounded per-frame timer queue is retained by the hub. + hub.purge_expired(now_unix_ms()).await; + assert!(hub.inner.latest.read().await.is_none()); + assert!(hub.inner.history.read().await.is_empty()); + } +} diff --git a/v2/crates/ruview-nlos/src/simulator.rs b/v2/crates/ruview-nlos/src/simulator.rs new file mode 100644 index 00000000..e9fc6859 --- /dev/null +++ b/v2/crates/ruview-nlos/src/simulator.rs @@ -0,0 +1,343 @@ +//! Deterministic L0 simulator and comparative fusion benchmark. + +use std::time::Instant; + +use serde::{Deserialize, Serialize}; + +use crate::calibration::{Calibration, CalibrationConfig}; +use crate::fusion::{CsiSpatialPrior, FusionScope}; +use crate::protocol::{ + EvidenceLevel, FrameSource, Provenance, SensorPose, TrackState, TransientFrame, TransientKind, + TransientZone, Vec3, +}; +use crate::tracker::{CanonicalObject, MotionApertureTracker, TrackerConfig}; +use crate::TRANSIENT_SCHEMA_V1; + +const C: f32 = 299_792_458.0; + +/// Controlled photon-histogram generator. It is intentionally labelled L0 and +/// cannot substitute for a VL53L8CH hardware capture. +pub struct SyntheticScene { + /// Timing width. + pub bin_width_ps: f32, + /// Timing bins. + pub bin_count: usize, + /// Relay-wall samples. + pub wall_points_m: Vec, + /// Direct return peak index. + pub direct_peak_bin: usize, + /// Direct peak photon count. + pub direct_amplitude: u16, + /// Third-bounce target count scale. + pub target_amplitude: f32, + rng: SimRng, +} + +impl Default for SyntheticScene { + fn default() -> Self { + let mut wall_points_m = Vec::new(); + for row in 0..4 { + for col in 0..4 { + wall_points_m.push(Vec3::new( + -0.45 + col as f32 * 0.3, + -0.45 + row as f32 * 0.3, + 0.0, + )); + } + } + Self { + bin_width_ps: 250.0, + bin_count: 48, + wall_points_m, + direct_peak_bin: 3, + direct_amplitude: 600, + target_amplitude: 900.0, + rng: SimRng::new(0x4e4c_4f53), + } + } +} + +impl SyntheticScene { + /// Generate an empty-room calibration sequence. + pub fn background_frames(&mut self, count: usize) -> Vec { + (0..count) + .map(|sequence| self.frame(None, 0.0, sequence as u64)) + .collect() + } + + /// Generate one raw transient frame. `visibility` scales only the weak + /// third-bounce return and can model optical dropout. + pub fn frame( + &mut self, + target: Option, + visibility: f32, + sequence: u64, + ) -> TransientFrame { + let aperture_shift = 0.018 * (sequence as f32 * 0.31).sin(); + let pose = SensorPose { + translation_m: Vec3::new(aperture_shift, 0.0, 0.0), + quaternion_xyzw: [0.0, 0.0, 0.0, 1.0], + }; + let zones = self + .wall_points_m + .clone() + .into_iter() + .enumerate() + .map(|(zone_id, wall)| { + let mut histogram = vec![0_u16; self.bin_count]; + for value in &mut histogram { + *value = 8 + (self.rng.next_u32() % 5) as u16; + } + for (offset, scale) in [(-1_i32, 0.25_f32), (0, 1.0), (1, 0.25)] { + let index = self.direct_peak_bin as i32 + offset; + if (0..self.bin_count as i32).contains(&index) { + histogram[index as usize] = histogram[index as usize] + .saturating_add((f32::from(self.direct_amplitude) * scale) as u16); + } + } + if let Some(target) = target { + let world_wall = pose.transform(wall); + let distance = world_wall.distance(target).max(0.05); + let extra_bin = + (2.0 * distance / (C * self.bin_width_ps * 1e-12)).round() as usize; + let target_bin = self.direct_peak_bin + extra_bin; + if target_bin < self.bin_count { + let amplitude = (self.target_amplitude * visibility / distance.powi(4)) + .clamp(0.0, 20_000.0); + for (offset, scale) in [(-1_i32, 0.4_f32), (0, 1.0), (1, 0.4)] { + let index = target_bin as i32 + offset; + if (0..self.bin_count as i32).contains(&index) { + histogram[index as usize] = histogram[index as usize] + .saturating_add((amplitude * scale) as u16); + } + } + } + } + TransientZone { + zone_id: zone_id as u16, + wall_point_m: wall, + distance_m: 0.8, + ambient: 8, + histogram, + } + }) + .collect(); + let frame = TransientFrame { + schema: TRANSIENT_SCHEMA_V1.into(), + session_id: "synthetic-nlos-1".into(), + sequence, + captured_at_unix_ms: 1_800_000_000_000 + sequence * 33, + monotonic_ns: sequence * 33_333_333, + source: FrameSource::Synthetic, + evidence_level: EvidenceLevel::L0Synthetic, + bin_width_ps: self.bin_width_ps, + start_bin: 30, + sensor_pose: pose, + calibration_hash: "0".repeat(64), + provenance: Provenance { + sensor_id: "sim-vl53l8ch".into(), + sensor_model: "VL53L8CH-simulator".into(), + firmware_version: "sim-v1".into(), + transient_kind: TransientKind::Replay, + histogram_preserved: true, + transport: "replay".into(), + }, + zones, + }; + debug_assert!(frame.validate().is_ok()); + frame + } + + /// Run a deterministic LiDAR-only versus LiDAR-plus-CSI comparison. + pub fn benchmark(frames: usize, particles: usize) -> BenchmarkReport { + let mut scene = Self::default(); + let fusion_scope = FusionScope { + tenant_id: "synthetic-tenant".into(), + workspace_id: "synthetic-workspace".into(), + site_id: "synthetic-site".into(), + world_frame_id: "synthetic-world".into(), + session_id: "synthetic-nlos-1".into(), + coordinate_transform_hash: "f".repeat(64), + }; + let calibration = Calibration::from_background( + &scene.background_frames(60), + CalibrationConfig::default(), + ) + .expect("synthetic calibration is valid"); + let config = TrackerConfig { + particle_count: particles, + search_min_m: Vec3::new(-0.6, -0.5, 0.35), + search_max_m: Vec3::new(0.6, 0.5, 1.5), + fusion_scope: Some(fusion_scope.clone()), + ..TrackerConfig::default() + }; + let mut lidar = MotionApertureTracker::new( + calibration.clone(), + CanonicalObject::point(), + config.clone(), + ) + .expect("benchmark config is valid"); + let mut fused = MotionApertureTracker::new(calibration, CanonicalObject::point(), config) + .expect("benchmark config is valid"); + let mut lidar_errors = Vec::new(); + let mut fused_errors = Vec::new(); + let mut lidar_lost = 0_usize; + let mut fused_lost = 0_usize; + let started = Instant::now(); + for index in 0..frames { + let t = index as f32 / frames.max(1) as f32; + let truth = Vec3::new( + -0.32 + 0.64 * t, + 0.12 * (t * std::f32::consts::TAU).sin(), + 0.92 + 0.08 * (t * std::f32::consts::TAU * 0.5).cos(), + ); + // Twelve-frame optical dropouts outlast the eight-frame aperture, + // making lost-track recovery measurable rather than cosmetic. + let dropout = index % 24 < 12; + let visibility = if dropout { 0.005 } else { 1.0 }; + let frame = scene.frame(Some(truth), visibility, 100 + index as u64); + let csi = CsiSpatialPrior { + source: FrameSource::Synthetic, + sequence: index as u64, + captured_at_unix_ms: frame.captured_at_unix_ms, + scope: fusion_scope.clone(), + mean_m: Vec3::new( + truth.x + 0.025 * (index as f32 * 0.7).sin(), + truth.y + 0.03 * (index as f32 * 0.4).cos(), + truth.z + 0.04 * (index as f32 * 0.2).sin(), + ), + covariance_diagonal_m2: Vec3::new(0.04, 0.04, 0.09), + confidence: 0.72, + evidence_level: EvidenceLevel::L0Synthetic, + sensor_id: "sim-csi-1".into(), + calibration_hash: "0".repeat(64), + }; + let lidar_output = lidar.update(&frame, None).expect("ordered frame"); + let fused_output = fused.update(&frame, Some(&csi)).expect("valid prior"); + collect_metric(&lidar_output, truth, &mut lidar_errors, &mut lidar_lost); + collect_metric(&fused_output, truth, &mut fused_errors, &mut fused_lost); + } + let elapsed = started.elapsed(); + let lidar_lost_rate = lidar_lost as f32 / frames.max(1) as f32; + let fused_lost_rate = fused_lost as f32 / frames.max(1) as f32; + let lost_track_reduction_percent = if lidar_lost_rate > 0.0 { + 100.0 * (lidar_lost_rate - fused_lost_rate) / lidar_lost_rate + } else { + 0.0 + }; + BenchmarkReport { + evidence: "SYNTHETIC_L0".into(), + frames, + particles, + throughput_fps: frames as f64 / elapsed.as_secs_f64().max(1e-9), + lidar_only_mean_error_m: mean(&lidar_errors), + fused_mean_error_m: mean(&fused_errors), + lidar_only_p95_error_m: percentile95(&mut lidar_errors), + fused_p95_error_m: percentile95(&mut fused_errors), + lidar_only_lost_track_rate: lidar_lost_rate, + fused_lost_track_rate: fused_lost_rate, + lost_track_reduction_percent, + hardware_reproduction_gate_passed: false, + } + } +} + +fn collect_metric( + envelope: &crate::protocol::TrackEnvelope, + truth: Vec3, + errors: &mut Vec, + lost: &mut usize, +) { + let track = &envelope.tracks[0]; + if track.state == TrackState::Tracking { + errors.push(track.position_m.distance(truth)); + } else { + *lost += 1; + } +} + +fn mean(values: &[f32]) -> f32 { + if values.is_empty() { + f32::INFINITY + } else { + values.iter().sum::() / values.len() as f32 + } +} + +fn percentile95(values: &mut [f32]) -> f32 { + if values.is_empty() { + return f32::INFINITY; + } + values.sort_by(f32::total_cmp); + values[((values.len() - 1) as f32 * 0.95).round() as usize] +} + +/// Reproducible benchmark result. The hardware gate always remains false for +/// this simulator regardless of performance. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BenchmarkReport { + /// Explicit evidence watermark. + pub evidence: String, + /// Frames evaluated. + pub frames: usize, + /// Particles per tracker. + pub particles: usize, + /// Combined LiDAR-only and fused updates per wall-clock second. + pub throughput_fps: f64, + /// LiDAR-only mean error over non-lost frames. + pub lidar_only_mean_error_m: f32, + /// Fused mean error over non-lost frames. + pub fused_mean_error_m: f32, + /// LiDAR-only p95 error. + pub lidar_only_p95_error_m: f32, + /// Fused p95 error. + pub fused_p95_error_m: f32, + /// LiDAR-only lost-track fraction. + pub lidar_only_lost_track_rate: f32, + /// Fused lost-track fraction. + pub fused_lost_track_rate: f32, + /// Relative lost-track reduction. + pub lost_track_reduction_percent: f32, + /// Always false for generated input. + pub hardware_reproduction_gate_passed: bool, +} + +#[derive(Clone, Debug)] +struct SimRng(u64); + +impl SimRng { + fn new(seed: u64) -> Self { + Self(seed) + } + + fn next_u32(&mut self) -> u32 { + self.0 = self + .0 + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1); + (self.0 >> 32) as u32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_frames_are_always_l0_and_preserve_histograms() { + let mut scene = SyntheticScene::default(); + let frame = scene.frame(Some(Vec3::new(0.0, 0.0, 1.0)), 1.0, 1); + assert_eq!(frame.source, FrameSource::Synthetic); + assert_eq!(frame.evidence_level, EvidenceLevel::L0Synthetic); + assert!(frame.provenance.histogram_preserved); + frame.validate().unwrap(); + } + + #[test] + fn benchmark_never_promotes_synthetic_to_hardware_evidence() { + let report = SyntheticScene::benchmark(40, 128); + assert_eq!(report.evidence, "SYNTHETIC_L0"); + assert!(!report.hardware_reproduction_gate_passed); + } +} diff --git a/v2/crates/ruview-nlos/src/tracker.rs b/v2/crates/ruview-nlos/src/tracker.rs new file mode 100644 index 00000000..c4e2d166 --- /dev/null +++ b/v2/crates/ruview-nlos/src/tracker.rs @@ -0,0 +1,654 @@ +//! Motion-induced aperture particle tracking with optional CSI prior. + +use std::collections::VecDeque; + +use thiserror::Error; + +use crate::calibration::{Calibration, CalibrationError, PreprocessedFrame}; +use crate::fusion::{CsiSpatialPrior, FusionError, FusionScope}; +use crate::protocol::{ + FrameSource, ModalityContributions, NlosTrack, TrackEnvelope, TrackState, TransientFrame, Vec3, +}; +use crate::TRACK_SCHEMA_V1; + +const SPEED_OF_LIGHT_MPS: f32 = 299_792_458.0; +const ALGORITHM_VERSION: &str = "motion-aperture-canonical-v1"; +const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; + +/// A known rigid object represented as weighted points around its origin. +#[derive(Clone, Debug)] +pub struct CanonicalObject { + /// Canonical local-space points. + pub points_m: Vec, + /// Relative non-negative return weight for each point. + pub weights: Vec, +} + +impl CanonicalObject { + /// A single point reflector used for the first reproduction milestone. + #[must_use] + pub fn point() -> Self { + Self { + points_m: vec![Vec3::default()], + weights: vec![1.0], + } + } + + fn validate(&self) -> Result<(), TrackerError> { + if self.points_m.is_empty() + || self.points_m.len() > 4_096 + || self.points_m.len() != self.weights.len() + || self.points_m.iter().any(|point| !point.finite()) + || self + .weights + .iter() + .any(|weight| !weight.is_finite() || *weight < 0.0) + || self.weights.iter().all(|weight| *weight == 0.0) + { + return Err(TrackerError::InvalidConfig); + } + Ok(()) + } +} + +/// Search volume and quality gates. +#[derive(Clone, Debug)] +pub struct TrackerConfig { + /// Particle count; 1,000 matches the public reproduction default. + pub particle_count: usize, + /// Inclusive search minimum. + pub search_min_m: Vec3, + /// Inclusive search maximum. + pub search_max_m: Vec3, + /// Gaussian random-walk standard deviation per frame. + pub motion_std_m: f32, + /// Likelihood sharpening exponent. + pub eta: f32, + /// Maximum aperture samples retained. + pub aperture_frames: usize, + /// Minimum combined signal quality for `tracking`. + pub min_signal_quality: f32, + /// Minimum posterior confidence for `tracking`. + pub min_confidence: f32, + /// Maximum CSI-to-optical time difference. + pub max_csi_age_ms: u64, + /// Output freshness window. + pub output_ttl_ms: u64, + /// Deterministic particle RNG seed. + pub seed: u64, + /// Exact policy/coordinate scope for synthetic fusion tests. Measured + /// fusion remains blocked by the v1 lineage boundary. + pub fusion_scope: Option, +} + +impl Default for TrackerConfig { + fn default() -> Self { + Self { + particle_count: 1_000, + search_min_m: Vec3::new(-1.0, -0.8, 0.1), + search_max_m: Vec3::new(1.0, 0.8, 1.8), + motion_std_m: 0.05, + eta: 3.0, + aperture_frames: 8, + min_signal_quality: 0.25, + min_confidence: 0.08, + max_csi_age_ms: 100, + output_ttl_ms: 250, + seed: 42, + fusion_scope: None, + } + } +} + +impl TrackerConfig { + fn validate(&self) -> Result<(), TrackerError> { + if !(64..=20_000).contains(&self.particle_count) + || !self.search_min_m.finite() + || !self.search_max_m.finite() + || self.search_min_m.x >= self.search_max_m.x + || self.search_min_m.y >= self.search_max_m.y + || self.search_min_m.z >= self.search_max_m.z + || [ + self.search_min_m.x, + self.search_min_m.y, + self.search_min_m.z, + self.search_max_m.x, + self.search_max_m.y, + self.search_max_m.z, + ] + .iter() + .any(|value| value.abs() > 100.0) + || self.search_max_m.x - self.search_min_m.x > 6.0 + || self.search_max_m.y - self.search_min_m.y > 6.0 + || self.search_max_m.z - self.search_min_m.z > 6.0 + || !self.motion_std_m.is_finite() + || !(0.001..=0.5).contains(&self.motion_std_m) + || !self.eta.is_finite() + || !(0.1..=16.0).contains(&self.eta) + || !(1..=32).contains(&self.aperture_frames) + || !(0.0..=1.0).contains(&self.min_signal_quality) + || !(0.0..=1.0).contains(&self.min_confidence) + || self.max_csi_age_ms > 5_000 + || !(1..=5_000).contains(&self.output_ttl_ms) + || self + .fusion_scope + .as_ref() + .is_some_and(|scope| scope.validate().is_err()) + { + return Err(TrackerError::InvalidConfig); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug)] +struct Particle { + position: Vec3, + velocity: Vec3, + weight: f32, +} + +/// Stateful single-target motion-induced aperture tracker. +pub struct MotionApertureTracker { + calibration: Calibration, + canonical: CanonicalObject, + config: TrackerConfig, + particles: Vec, + aperture: VecDeque, + last_sequence: Option, + last_monotonic_ns: Option, + last_csi_sequence: Option, + rng: DeterministicRng, +} + +impl MotionApertureTracker { + /// Construct a tracker with a deterministic uniform prior. + pub fn new( + calibration: Calibration, + canonical: CanonicalObject, + config: TrackerConfig, + ) -> Result { + config.validate()?; + canonical.validate()?; + if config + .fusion_scope + .as_ref() + .is_some_and(|scope| scope.session_id != calibration.session_id()) + { + return Err(TrackerError::InvalidConfig); + } + let work = config + .particle_count + .checked_mul(canonical.points_m.len()) + .and_then(|value| value.checked_mul(calibration.zone_count())) + .and_then(|value| value.checked_mul(config.aperture_frames)) + .ok_or(TrackerError::InvalidConfig)?; + if work > 100_000_000 { + return Err(TrackerError::InvalidConfig); + } + let mut rng = DeterministicRng::new(config.seed); + let uniform = 1.0 / config.particle_count as f32; + let particles = (0..config.particle_count) + .map(|_| Particle { + position: random_point(&mut rng, config.search_min_m, config.search_max_m), + velocity: Vec3::default(), + weight: uniform, + }) + .collect(); + Ok(Self { + calibration, + canonical, + config, + particles, + aperture: VecDeque::new(), + last_sequence: None, + last_monotonic_ns: None, + last_csi_sequence: None, + rng, + }) + } + + /// Process one ordered transient frame and optional calibrated CSI prior. + pub fn update( + &mut self, + frame: &TransientFrame, + csi_prior: Option<&CsiSpatialPrior>, + ) -> Result { + let expires_at_unix_ms = frame + .captured_at_unix_ms + .checked_add(self.config.output_ttl_ms) + .filter(|value| *value <= MAX_SAFE_INTEGER) + .ok_or(TrackerError::TimestampOverflow)?; + if self + .last_sequence + .is_some_and(|last| frame.sequence <= last) + || self + .last_monotonic_ns + .is_some_and(|last| frame.monotonic_ns <= last) + { + return Err(TrackerError::ReplayOrOutOfOrder); + } + let delta_seconds = self.last_monotonic_ns.map_or(1.0 / 30.0, |last| { + ((frame.monotonic_ns - last) as f32 * 1e-9).clamp(1.0 / 240.0, 0.5) + }); + let processed = self.calibration.preprocess(frame)?; + let csi = if let Some(prior) = csi_prior { + prior.validate()?; + let expected_scope = self + .config + .fusion_scope + .as_ref() + .ok_or(FusionError::BindingMismatch)?; + if &prior.scope != expected_scope + || frame.session_id != expected_scope.session_id + || prior.source != frame.source + { + return Err(FusionError::BindingMismatch.into()); + } + if frame.source != FrameSource::Synthetic { + return Err(FusionError::MeasuredFusionUnavailable.into()); + } + if self + .last_csi_sequence + .is_some_and(|last| prior.sequence <= last) + { + return Err(FusionError::ReplayOrOutOfOrder.into()); + } + let age = frame + .captured_at_unix_ms + .abs_diff(prior.captured_at_unix_ms); + if age > self.config.max_csi_age_ms { + return Err(TrackerError::Fusion(FusionError::Stale)); + } + Some(prior) + } else { + None + }; + self.last_sequence = Some(frame.sequence); + self.last_monotonic_ns = Some(frame.monotonic_ns); + if let Some(prior) = csi { + self.last_csi_sequence = Some(prior.sequence); + } + self.aperture.push_back(processed); + while self.aperture.len() > self.config.aperture_frames { + self.aperture.pop_front(); + } + + self.propagate(delta_seconds); + let mut log_weights = Vec::with_capacity(self.particles.len()); + let mut max_log = f32::NEG_INFINITY; + for particle in &self.particles { + let lidar_score = self.score_particle(particle, frame.monotonic_ns).max(1e-9); + let mut log_weight = self.config.eta * lidar_score.ln(); + if let Some(prior) = csi { + log_weight += prior.log_likelihood(particle.position); + } + max_log = max_log.max(log_weight); + log_weights.push(log_weight); + } + let mut sum = 0.0_f32; + for (particle, log_weight) in self.particles.iter_mut().zip(log_weights) { + particle.weight = (log_weight - max_log).exp(); + sum += particle.weight; + } + if !sum.is_finite() || sum <= f32::EPSILON { + for particle in &mut self.particles { + particle.weight = 1.0 / self.config.particle_count as f32; + } + } else { + for particle in &mut self.particles { + particle.weight /= sum; + } + } + + let envelope = self.posterior(frame, csi, expires_at_unix_ms); + self.systematic_resample(); + Ok(envelope) + } + + fn propagate(&mut self, delta_seconds: f32) { + let min = self.config.search_min_m; + let max = self.config.search_max_m; + for particle in &mut self.particles { + let innovation = Vec3::new( + self.rng.gaussian() * self.config.motion_std_m, + self.rng.gaussian() * self.config.motion_std_m, + self.rng.gaussian() * self.config.motion_std_m, + ); + let previous_position = particle.position; + let proposed = particle + .position + .plus(particle.velocity.scale(delta_seconds)) + .plus(innovation); + particle.position = clamp_vec(proposed, min, max); + let observed_velocity = particle + .position + .minus(previous_position) + .scale(1.0 / delta_seconds); + particle.velocity = clamp_vec( + particle + .velocity + .scale(0.65) + .plus(observed_velocity.scale(0.35)), + Vec3::new(-20.0, -20.0, -20.0), + Vec3::new(20.0, 20.0, 20.0), + ); + } + } + + fn score_particle(&self, particle: &Particle, current_monotonic_ns: u64) -> f32 { + let mut aperture_score = 0.0_f32; + let mut valid_frames = 0_usize; + for frame in &self.aperture { + let aperture_age_seconds = + current_monotonic_ns.saturating_sub(frame.monotonic_ns) as f32 * 1e-9; + let historical_origin = particle + .position + .minus(particle.velocity.scale(aperture_age_seconds)); + let observed_norm = frame + .light_cone_histograms + .iter() + .map(|value| value * value) + .sum::() + .sqrt(); + if observed_norm <= f32::EPSILON { + continue; + } + let native_denominator = (frame.bin_count - 1).max(1) as f32; + let mut dot = 0.0_f32; + let mut predicted_norm_sq = 0.0_f32; + for (zone_index, wall) in frame.wall_points_world_m.iter().enumerate() { + for (point, weight) in self + .canonical + .points_m + .iter() + .zip(self.canonical.weights.iter()) + { + let target = historical_origin.plus(*point); + let distance = wall.distance(target).max(0.01); + let extra_bin = (2.0 * distance + / (SPEED_OF_LIGHT_MPS * frame.bin_width_ps * 1e-12)) + .round(); + let v_bin = ((extra_bin * extra_bin / native_denominator).floor() as usize) + .min(frame.bin_count - 1); + let amplitude = *weight / distance.powi(4).max(1e-4); + for (offset, kernel) in [(-1_i32, 0.5_f32), (0, 1.0), (1, 0.5)] { + let index = v_bin as i32 + offset; + if !(0..frame.bin_count as i32).contains(&index) { + continue; + } + let predicted = amplitude * kernel; + dot += frame.light_cone_histograms + [zone_index * frame.bin_count + index as usize] + * predicted; + predicted_norm_sq += predicted * predicted; + } + } + } + let denom = observed_norm * predicted_norm_sq.sqrt(); + if denom > f32::EPSILON { + aperture_score += (dot / denom).clamp(0.0, 1.0); + valid_frames += 1; + } + } + if valid_frames == 0 { + 0.0 + } else { + aperture_score / valid_frames as f32 + } + } + + fn posterior( + &self, + frame: &TransientFrame, + csi: Option<&CsiSpatialPrior>, + expires_at_unix_ms: u64, + ) -> TrackEnvelope { + let mut position = Vec3::default(); + let mut velocity = Vec3::default(); + let mut entropy = 0.0_f32; + for particle in &self.particles { + position = position.plus(particle.position.scale(particle.weight)); + velocity = velocity.plus(particle.velocity.scale(particle.weight)); + if particle.weight > 0.0 { + entropy -= particle.weight * particle.weight.ln(); + } + } + let mut covariance = Vec3::default(); + for particle in &self.particles { + let delta = particle.position.minus(position); + covariance.x += particle.weight * delta.x * delta.x; + covariance.y += particle.weight * delta.y * delta.y; + covariance.z += particle.weight * delta.z * delta.z; + } + let normalized_entropy = entropy / (self.particles.len() as f32).ln().max(1.0); + let posterior_focus = (1.0 - normalized_entropy).clamp(0.0, 1.0); + let lidar_quality = self + .aperture + .iter() + .map(|sample| sample.signal_quality) + .sum::() + / self.aperture.len().max(1) as f32; + let csi_quality = csi.map_or(0.0, |prior| prior.confidence); + let signal_quality = 1.0 - (1.0 - lidar_quality) * (1.0 - 0.6 * csi_quality); + let confidence = (posterior_focus * 1.4 + signal_quality * 0.45).clamp(0.0, 1.0); + let optical_present = lidar_quality > 1e-6; + let state = if optical_present + && signal_quality >= self.config.min_signal_quality + && confidence >= self.config.min_confidence + { + TrackState::Tracking + } else if optical_present && signal_quality >= self.config.min_signal_quality * 0.5 { + TrackState::Degraded + } else { + TrackState::Unknown + }; + let lidar_contribution = if csi.is_some() { + lidar_quality / (lidar_quality + csi_quality + f32::EPSILON) + } else { + 1.0 + }; + let csi_contribution = if csi.is_some() { + 1.0 - lidar_contribution + } else { + 0.0 + }; + // v1 cannot retain both modality lineages and therefore never promotes + // measured output to L3. The only accepted CSI path is synthetic L0. + let evidence_level = frame.evidence_level; + let mut provenance = frame.provenance.clone(); + provenance.transport = if frame.source == FrameSource::Live { + "ruview_server".into() + } else { + "replay".into() + }; + let output_calibration_hash = if frame.source == FrameSource::Synthetic { + "0".repeat(64) + } else { + self.calibration.hash().to_owned() + }; + let envelope = TrackEnvelope { + schema: TRACK_SCHEMA_V1.into(), + session_id: frame.session_id.clone(), + sequence: frame.sequence, + captured_at_unix_ms: frame.captured_at_unix_ms, + expires_at_unix_ms, + source: frame.source, + evidence_level, + algorithm_version: ALGORITHM_VERSION.into(), + calibration_hash: output_calibration_hash, + provenance, + tracks: vec![NlosTrack { + track_id: "hidden-target-0".into(), + state, + position_m: position, + velocity_mps: velocity, + covariance_diagonal_m2: covariance, + confidence, + posterior_entropy: entropy, + signal_quality, + modality_contributions: ModalityContributions { + lidar: lidar_contribution.clamp(0.0, 1.0), + csi: csi_contribution.clamp(0.0, 1.0), + }, + }], + }; + debug_assert!(envelope.validate().is_ok()); + envelope + } + + fn systematic_resample(&mut self) { + let count = self.particles.len(); + let step = 1.0 / count as f32; + let start = self.rng.uniform() * step; + let mut cumulative = self.particles[0].weight; + let mut index = 0_usize; + let mut next = Vec::with_capacity(count); + for sample in 0..count { + let threshold = start + sample as f32 * step; + while threshold > cumulative && index + 1 < count { + index += 1; + cumulative += self.particles[index].weight; + } + let mut particle = self.particles[index]; + particle.weight = step; + next.push(particle); + } + self.particles = next; + } +} + +fn random_point(rng: &mut DeterministicRng, min: Vec3, max: Vec3) -> Vec3 { + Vec3::new( + min.x + rng.uniform() * (max.x - min.x), + min.y + rng.uniform() * (max.y - min.y), + min.z + rng.uniform() * (max.z - min.z), + ) +} + +fn clamp_vec(value: Vec3, min: Vec3, max: Vec3) -> Vec3 { + Vec3::new( + value.x.clamp(min.x, max.x), + value.y.clamp(min.y, max.y), + value.z.clamp(min.z, max.z), + ) +} + +#[derive(Clone, Debug)] +struct DeterministicRng { + state: u64, + spare_gaussian: Option, +} + +impl DeterministicRng { + fn new(seed: u64) -> Self { + Self { + state: seed, + spare_gaussian: None, + } + } + + fn next_u64(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) + } + + fn uniform(&mut self) -> f32 { + let bits = (self.next_u64() >> 40) as u32; + (bits as f32 + 0.5) / 16_777_216.0 + } + + fn gaussian(&mut self) -> f32 { + if let Some(value) = self.spare_gaussian.take() { + return value; + } + let u1 = self.uniform().max(f32::EPSILON); + let u2 = self.uniform(); + let radius = (-2.0 * u1.ln()).sqrt(); + let angle = std::f32::consts::TAU * u2; + self.spare_gaussian = Some(radius * angle.sin()); + radius * angle.cos() + } +} + +/// Tracking failure at a fail-closed boundary. +#[derive(Debug, Error)] +pub enum TrackerError { + /// Tracker or canonical-object bounds are invalid. + #[error("invalid tracker configuration")] + InvalidConfig, + /// Sequence repeated or moved backwards. + #[error("replayed or out-of-order transient frame")] + ReplayOrOutOfOrder, + /// Capture time plus output TTL cannot be represented by the v1 JSON-safe + /// integer contract. + #[error("track expiry exceeds the v1 timestamp range")] + TimestampOverflow, + /// Calibration/preprocessing failure. + #[error(transparent)] + Calibration(#[from] CalibrationError), + /// CSI validation or freshness failure. + #[error(transparent)] + Fusion(#[from] FusionError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::calibration::{Calibration, CalibrationConfig}; + use crate::simulator::SyntheticScene; + + #[test] + fn replayed_sequence_is_rejected() { + let mut scene = SyntheticScene::default(); + let calibration = Calibration::from_background( + &scene.background_frames(20), + CalibrationConfig::default(), + ) + .unwrap(); + let mut tracker = MotionApertureTracker::new( + calibration, + CanonicalObject::point(), + TrackerConfig::default(), + ) + .unwrap(); + let frame = scene.frame(Some(Vec3::new(0.0, 0.0, 1.0)), 1.0, 100); + tracker.update(&frame, None).unwrap(); + assert!(matches!( + tracker.update(&frame, None), + Err(TrackerError::ReplayOrOutOfOrder) + )); + + let mut advanced_sequence = frame; + advanced_sequence.sequence += 1; + assert!(matches!( + tracker.update(&advanced_sequence, None), + Err(TrackerError::ReplayOrOutOfOrder) + )); + } + + #[test] + fn max_safe_capture_time_fails_closed_before_expiry_overflow() { + let mut scene = SyntheticScene::default(); + let calibration = Calibration::from_background( + &scene.background_frames(20), + CalibrationConfig::default(), + ) + .unwrap(); + let mut tracker = MotionApertureTracker::new( + calibration, + CanonicalObject::point(), + TrackerConfig::default(), + ) + .unwrap(); + let mut frame = scene.frame(Some(Vec3::new(0.0, 0.0, 1.0)), 1.0, 100); + frame.captured_at_unix_ms = MAX_SAFE_INTEGER; + assert!(matches!( + tracker.update(&frame, None), + Err(TrackerError::TimestampOverflow) + )); + } +} diff --git a/v2/crates/ruview-nlos/tests/acceptance.rs b/v2/crates/ruview-nlos/tests/acceptance.rs new file mode 100644 index 00000000..6a3fd765 --- /dev/null +++ b/v2/crates/ruview-nlos/tests/acceptance.rs @@ -0,0 +1,40 @@ +//! Deterministic software acceptance. All inputs are SYNTHETIC/L0; this proves +//! contract and fusion behavior, not the ADR-331 hardware reproduction gate. + +use ruview_nlos::protocol::{EvidenceLevel, FrameSource}; +use ruview_nlos::{SyntheticScene, TrackEnvelope}; + +#[test] +fn golden_track_contract_round_trips_byte_semantics() { + let fixture = include_str!("fixtures/track_synthetic.json"); + let envelope: TrackEnvelope = serde_json::from_str(fixture).unwrap(); + envelope.validate().unwrap(); + let encoded = serde_json::to_string(&envelope).unwrap(); + let decoded: TrackEnvelope = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded, envelope); +} + +#[test] +fn synthetic_fusion_reduces_lost_tracks_by_at_least_25_percent() { + let report = SyntheticScene::benchmark(120, 512); + assert_eq!(report.evidence, "SYNTHETIC_L0"); + assert!(!report.hardware_reproduction_gate_passed); + assert!( + report.lost_track_reduction_percent >= 25.0, + "SYNTHETIC architecture gate: LiDAR-only lost rate={}, fused lost rate={}, reduction={}%; this is not hardware evidence", + report.lidar_only_lost_track_rate, + report.fused_lost_track_rate, + report.lost_track_reduction_percent, + ); +} + +#[test] +fn generated_contract_can_never_alias_to_live_evidence() { + let mut scene = SyntheticScene::default(); + let frame = scene.frame(None, 0.0, 1); + assert_eq!(frame.source, FrameSource::Synthetic); + assert_eq!(frame.evidence_level, EvidenceLevel::L0Synthetic); + assert_eq!(frame.calibration_hash, "0".repeat(64)); + assert_eq!(frame.provenance.transport, "replay"); + frame.validate().unwrap(); +} diff --git a/v2/crates/ruview-nlos/tests/fixtures/track_synthetic.json b/v2/crates/ruview-nlos/tests/fixtures/track_synthetic.json new file mode 100644 index 00000000..74f5ee35 --- /dev/null +++ b/v2/crates/ruview-nlos/tests/fixtures/track_synthetic.json @@ -0,0 +1,32 @@ +{ + "schema": "ruview.nlos.track.v1", + "sessionId": "synthetic-contract-1", + "sequence": 42, + "capturedAtUnixMs": 1800000000000, + "expiresAtUnixMs": 1800000000250, + "source": "synthetic", + "evidenceLevel": "l0_synthetic", + "algorithmVersion": "motion-aperture-canonical-v1", + "calibrationHash": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": { + "sensorId": "sim-vl53l8ch", + "sensorModel": "VL53L8CH-simulator", + "firmwareVersion": "sim-v1", + "transientKind": "replay", + "histogramPreserved": true, + "transport": "replay" + }, + "tracks": [ + { + "trackId": "hidden-target-0", + "state": "tracking", + "positionM": { "x": 0.2, "y": -0.1, "z": 1.0 }, + "velocityMps": { "x": 0.01, "y": 0.0, "z": 0.0 }, + "covarianceDiagonalM2": { "x": 0.01, "y": 0.02, "z": 0.03 }, + "confidence": 0.75, + "posteriorEntropy": 2.0, + "signalQuality": 0.7, + "modalityContributions": { "lidar": 0.6, "csi": 0.4 } + } + ] +}