Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 30b7c23a91 Bump the actions group across 2 directories with 1 update
Bumps the actions group with 1 update in the / directory: [docker/login-action](https://github.com/docker/login-action).
Bumps the actions group with 1 update in the /.github/actions/setup directory: [docker/login-action](https://github.com/docker/login-action).


Updates `docker/login-action` from 4.7.0 to 4.8.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/81c6f93d4bc991e510b342a00b419b3bb4efc5c6...8aa5831fb8d48d8f76594c7ddde61ae58019f550)

Updates `docker/login-action` from 4.7.0 to 4.8.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/81c6f93d4bc991e510b342a00b419b3bb4efc5c6...8aa5831fb8d48d8f76594c7ddde61ae58019f550)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-08 11:33:13 +00:00
26 changed files with 275 additions and 708 deletions
+7
View File
@@ -4,6 +4,7 @@ set -euxo pipefail
NGINX_VERSION="1.27.4" NGINX_VERSION="1.27.4"
VOD_MODULE_VERSION="v1.9.1" VOD_MODULE_VERSION="v1.9.1"
SECURE_TOKEN_MODULE_VERSION="1.5"
SET_MISC_MODULE_VERSION="v0.33" SET_MISC_MODULE_VERSION="v0.33"
NGX_DEVEL_KIT_VERSION="v0.3.3" NGX_DEVEL_KIT_VERSION="v0.3.3"
@@ -51,6 +52,11 @@ patch -d /tmp/nginx-vod-module/ -p1 << 'EOF'
EOF EOF
mkdir /tmp/nginx-secure-token-module
wget https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz
tar -zxf ${SECURE_TOKEN_MODULE_VERSION}.tar.gz -C /tmp/nginx-secure-token-module --strip-components=1
rm ${SECURE_TOKEN_MODULE_VERSION}.tar.gz
mkdir /tmp/ngx_devel_kit mkdir /tmp/ngx_devel_kit
wget https://github.com/vision5/ngx_devel_kit/archive/refs/tags/${NGX_DEVEL_KIT_VERSION}.tar.gz wget https://github.com/vision5/ngx_devel_kit/archive/refs/tags/${NGX_DEVEL_KIT_VERSION}.tar.gz
tar -zxf ${NGX_DEVEL_KIT_VERSION}.tar.gz -C /tmp/ngx_devel_kit --strip-components=1 tar -zxf ${NGX_DEVEL_KIT_VERSION}.tar.gz -C /tmp/ngx_devel_kit --strip-components=1
@@ -74,6 +80,7 @@ cd /tmp/nginx
--add-module=../ngx_devel_kit \ --add-module=../ngx_devel_kit \
--add-module=../nginx-set-misc-module \ --add-module=../nginx-set-misc-module \
--add-module=../nginx-vod-module \ --add-module=../nginx-vod-module \
--add-module=../nginx-secure-token-module \
--with-cc-opt="-O3 -Wno-error=implicit-fallthrough" --with-cc-opt="-O3 -Wno-error=implicit-fallthrough"
make CC="ccache gcc" -j$(nproc) && make install make CC="ccache gcc" -j$(nproc) && make install
+5 -5
View File
@@ -7,18 +7,18 @@ starlette-context == 0.5.*
fastapi[standard-no-fastapi-cloud-cli] == 0.162.* fastapi[standard-no-fastapi-cloud-cli] == 0.162.*
uvicorn == 0.54.* uvicorn == 0.54.*
slowapi == 0.1.* slowapi == 0.1.*
joserfc == 1.8.* joserfc == 1.7.*
cryptography == 50.0.* cryptography == 50.0.*
pathvalidate == 3.3.* pathvalidate == 3.3.*
markupsafe == 3.0.* markupsafe == 3.0.*
python-multipart == 0.0.33 python-multipart == 0.0.32
# Classification Model Training # Classification Model Training
tensorflow == 2.19.* ; platform_machine == 'aarch64' tensorflow == 2.19.* ; platform_machine == 'aarch64'
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64' tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
# General # General
mypy == 2.4.1 mypy == 2.4.1
onvif-zeep-async == 4.3.* onvif-zeep-async == 4.3.*
paho-mqtt == 2.2.* paho-mqtt == 2.1.*
pandas == 3.0.* pandas == 3.0.*
peewee == 4.5.* peewee == 4.5.*
peewee_migrate == 2.3.* peewee_migrate == 2.3.*
@@ -26,7 +26,7 @@ psutil == 7.2.*
pydantic == 2.14.* pydantic == 2.14.*
git+https://github.com/fbcotter/py3nvml#egg=py3nvml git+https://github.com/fbcotter/py3nvml#egg=py3nvml
tzdata == 2026.6 tzdata == 2026.6
pyzmq == 27.3.* pyzmq == 27.2.*
ruamel.yaml == 0.19.* ruamel.yaml == 0.19.*
tzlocal == 5.4.5 tzlocal == 5.4.5
requests == 2.34.* requests == 2.34.*
@@ -49,7 +49,7 @@ transformers == 5.22.*
# Generative AI # Generative AI
google-genai == 2.34.* google-genai == 2.34.*
ollama == 0.6.* ollama == 0.6.*
openai == 3.46.* openai == 3.41.*
# push notifications # push notifications
py-vapid == 1.9.6 py-vapid == 1.9.6
pywebpush == 2.5.* pywebpush == 2.5.*
@@ -14,17 +14,11 @@ fi
# Tell S6-Overlay not to restart this service # Tell S6-Overlay not to restart this service
s6-svc -O . s6-svc -O .
# The helper runs as root (-I skips user site-packages). The probe runs as the # Runs as root with root's HOME and LD_LIBRARY_PATH; -I skips user site-packages
# service user when the service drops privileges; a root probe relies on
# resolve_ffmpeg_path refusing runtime-user-writable paths.
function set_libva_version() { function set_libva_version() {
local ffmpeg_path local ffmpeg_path
local probe_as=() ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py)
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
probe_as=(s6-setuidgid frigate)
fi
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py frigate)
LIBAVFORMAT_VERSION_MAJOR=$("${probe_as[@]}" "$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
export LIBAVFORMAT_VERSION_MAJOR export LIBAVFORMAT_VERSION_MAJOR
} }
@@ -57,19 +57,10 @@ function get_ip_and_port_from_supervisor() {
export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}" export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}"
} }
# The probe runs as the service user when the service drops privileges; a root
# probe relies on resolve_ffmpeg_path refusing runtime-user-writable paths
function set_libva_version() { function set_libva_version() {
local ffmpeg_path local ffmpeg_path
local probe_as=() ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py)
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
probe_as=(s6-setuidgid go2rtc)
fi
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py go2rtc)
if ! LIBAVFORMAT_VERSION_MAJOR=$("${probe_as[@]}" "$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+"); then
echo "[WARN] Could not read the libavformat version from ${ffmpeg_path} as the go2rtc user; using the bundled build's version"
LIBAVFORMAT_VERSION_MAJOR=$("${probe_as[@]}" "/usr/lib/ffmpeg/${DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" -version | grep -Po "libavformat\W+\K\d+")
fi
export LIBAVFORMAT_VERSION_MAJOR export LIBAVFORMAT_VERSION_MAJOR
} }
@@ -25,5 +25,4 @@ except FileNotFoundError:
config: dict[str, Any] = {} config: dict[str, Any] = {}
path = config.get("ffmpeg", {}).get("path", "default") path = config.get("ffmpeg", {}).get("path", "default")
service = sys.argv[1] if len(sys.argv) > 1 else "frigate" print(resolve_ffmpeg_path(path, "ffmpeg"))
print(resolve_ffmpeg_path(path, "ffmpeg", service))
@@ -40,10 +40,16 @@ try:
except FileNotFoundError: except FileNotFoundError:
config: dict[str, Any] = {} config: dict[str, Any] = {}
# No validator runs here, so install environment_vars ourselves. # No validator runs here, so install environment_vars ourselves. FRIGATE_
# names only: anything else lands in os.environ, where the exec gate reads
# GO2RTC_ALLOW_ARBITRARY_EXEC.
config_env_vars = config.get("environment_vars") config_env_vars = config.get("environment_vars")
apply_config_env_vars( apply_config_env_vars(
{str(key): value for key, value in config_env_vars.items()} {
key: value
for key, value in config_env_vars.items()
if str(key).startswith("FRIGATE_")
}
if isinstance(config_env_vars, dict) if isinstance(config_env_vars, dict)
else {} else {}
) )
@@ -97,7 +103,7 @@ if not is_valid_ffmpeg_path(path):
print( print(
"[ERROR] ffmpeg path contains spaces, quotes or shell characters, using the default ffmpeg instead." "[ERROR] ffmpeg path contains spaces, quotes or shell characters, using the default ffmpeg instead."
) )
ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg", "go2rtc") ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg")
for key in apply_go2rtc_ffmpeg_config(go2rtc_config, ffmpeg_path): for key in apply_go2rtc_ffmpeg_config(go2rtc_config, ffmpeg_path):
print( print(
@@ -143,14 +143,8 @@ http {
# version 6 (RFC 8216 section 7); the module default is 4 # version 6 (RFC 8216 section 7); the module default is 4
vod_hls_version 6; vod_hls_version 6;
# Pass the playlist's query args (e.g. Home Assistant's authSig) on secure_token $args;
# to every URL in it. These match the only URL endings vod writes secure_token_types application/vnd.apple.mpegurl;
# with fmp4 and no subtitles.
sub_filter_types application/vnd.apple.mpegurl;
sub_filter_once off;
sub_filter '.m3u8' '.m3u8$is_args$args';
sub_filter '.m4s' '.m4s$is_args$args';
sub_filter '.mp4"' '.mp4$is_args$args"';
include security_headers.conf; include security_headers.conf;
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
@@ -240,7 +240,7 @@ logger:
# Optional: set environment variables # Optional: set environment variables
environment_vars: environment_vars:
LIBVA_DRIVER_NAME: i965 EXAMPLE_VAR: value
# Optional: birdseye configuration # Optional: birdseye configuration
# NOTE: Can (enabled, mode) be overridden at the camera level # NOTE: Can (enabled, mode) be overridden at the camera level
+3 -12
View File
@@ -65,16 +65,7 @@ go2rtc:
This section sets environment variables in the Frigate process for those unable to modify the environment of the container, like within Home Assistant OS. It's meant for process settings such as `LIBVA_DRIVER_NAME` or the TensorFlow thread counts below. Docker users should set environment variables in their `docker run` command (`-e LIBVA_DRIVER_NAME=i965`) or `docker-compose.yml` file (`environment:` section) instead. Values set here are stored in plain text in your config file, so credentials belong in `secrets.yaml`, Docker environment variables, or Docker secrets instead. This section sets environment variables in the Frigate process for those unable to modify the environment of the container, like within Home Assistant OS. It's meant for process settings such as `LIBVA_DRIVER_NAME` or the TensorFlow thread counts below. Docker users should set environment variables in their `docker run` command (`-e LIBVA_DRIVER_NAME=i965`) or `docker-compose.yml` file (`environment:` section) instead. Values set here are stored in plain text in your config file, so credentials belong in `secrets.yaml`, Docker environment variables, or Docker secrets instead.
Besides `FRIGATE_` names, only these are accepted, and a variable already set in the container environment keeps its container value: Names prefixed with `FRIGATE_` set here also take part in `{FRIGATE_VARIABLE_NAME}` substitution (see [below](#substitution-sources-and-precedence)), but `secrets.yaml` is the better home for them.
- `LIBVA_DRIVER_NAME` and `HSA_OVERRIDE_GFX_VERSION`
- `TF_INTRA_OP_PARALLELISM_THREADS`, `TF_INTER_OP_PARALLELISM_THREADS`, `TF_DATASET_THREAD_POOL_SIZE`, and `SHM_MAX_FRAMES`
- `USE_FP16`, `HF_HUB_OFFLINE`, and `TRANSFORMERS_OFFLINE`
- `HF_ENDPOINT`, `GITHUB_ENDPOINT`, `GITHUB_RAW_ENDPOINT`, and `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` (an `http://` or `https://` URL)
Any other name is ignored with a warning and must be set in the container environment.
Names prefixed with `FRIGATE_` set here take part in `{FRIGATE_VARIABLE_NAME}` substitution (see [below](#substitution-sources-and-precedence)), but `secrets.yaml` is the better home for them. Of these, only `FRIGATE_JWT_SECRET` is also set in the process environment.
<ConfigTabs> <ConfigTabs>
<TabItem value="ui"> <TabItem value="ui">
@@ -407,9 +398,9 @@ To do this:
2. Update the `ffmpeg.path` in your Frigate config to `/config/custom-ffmpeg`. 2. Update the `ffmpeg.path` in your Frigate config to `/config/custom-ffmpeg`.
3. Restart Frigate and the custom version will be used if the steps above were done correctly. 3. Restart Frigate and the custom version will be used if the steps above were done correctly.
Both binaries have to be executable by Frigate's unprivileged runtime user, so `chmod 755` them after extracting. go2rtc runs as its own user, so the build must be world-executable for go2rtc to use it (`ffmpeg:` sources, the birdseye restream and transcoded live streams). The startup ownership sweep runs only once, so anything you add to `/config` later keeps whatever ownership and mode you gave it. Both binaries have to be executable by Frigate's unprivileged runtime user, so `chmod 755` them after extracting. The startup ownership sweep runs only once, so anything you add to `/config` later keeps whatever ownership and mode you gave it.
There is one exception, and it only affects [`FRIGATE_ROOT_SERVICES`](/configuration/non_root#keeping-individual-services-root) listing `frigate` or `go2rtc`. That mode runs the listed service as root while still handing `/config` to the unprivileged runtime user, so anything running as that user could swap the binary and gain root. A build inside any of Frigate's writable volumes (`/config`, `/media/frigate`, the cache and shm dirs) is ignored by the root service, which uses the bundled one and logs a warning. A service that is not listed keeps using the custom build. Keep the build somewhere root-owned (any absolute `ffmpeg.path` works, so a read-only bind mount such as `/opt/custom-ffmpeg` is enough) if you need both. The default mode and `FRIGATE_RUN_AS_ROOT=true` are unaffected and behave exactly as they always have. There is one exception, and it only affects [`FRIGATE_ROOT_SERVICES`](/configuration/non_root#keeping-individual-services-root) listing `frigate`. That mode runs Frigate as root while still handing `/config` to the unprivileged runtime user, so anything running as that user could swap the binary and gain root. A build inside any of Frigate's writable volumes (`/config`, `/media/frigate`, the cache and shm dirs) is ignored there and the bundled one is used, with a warning in the log. Keep the build somewhere root-owned (any absolute `ffmpeg.path` works, so a read-only bind mount such as `/opt/custom-ffmpeg` is enough) if you need both. The default mode and `FRIGATE_RUN_AS_ROOT=true` are unaffected and behave exactly as they always have.
### Custom go2rtc version ### Custom go2rtc version
-1
View File
@@ -627,7 +627,6 @@ def _restricted_go2rtc_response(
Go2rtcStreamIssue.invalid_placeholder, Go2rtcStreamIssue.invalid_placeholder,
"Invalid variable placeholder in go2rtc stream", "Invalid variable placeholder in go2rtc stream",
), ),
(Go2rtcStreamIssue.invalid_name, "Invalid go2rtc stream name"),
): ):
names = [name for name, found in issues.items() if found == issue] names = [name for name, found in issues.items() if found == issue]
if names: if names:
+1 -14
View File
@@ -39,7 +39,6 @@ from frigate.config import AuthConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User from frigate.models import User
from frigate.notices import raise_notice from frigate.notices import raise_notice
from frigate.util.live_streams import is_transcode_stream_name
from frigate.util.origin import origin_allowed from frigate.util.origin import origin_allowed
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -740,13 +739,6 @@ def auth(request: Request):
int(request.headers.get("x-server-port", default=0)) int(request.headers.get("x-server-port", default=0))
== request.app.auth_internal_port == request.app.auth_internal_port
): ):
# go2rtc runs the producer an unknown src describes for any caller
deny_status = deny_response_for_go2rtc_stream(
request.headers.get("x-original-url"), None, request
)
if deny_status is not None:
return Response("", status_code=deny_status)
success_response.headers["remote-user"] = "anonymous" success_response.headers["remote-user"] = "anonymous"
success_response.headers["remote-role"] = "admin" success_response.headers["remote-role"] = "admin"
return success_response return success_response
@@ -1283,12 +1275,7 @@ def _is_existing_go2rtc_stream(request: Request, stream_name: str) -> bool:
""" """
frigate_config = request.app.frigate_config frigate_config = request.app.frigate_config
# live.streams values are free-form config, so only names Frigate controls if _get_stream_owner_cameras(request, stream_name):
# skip the lookup
if any(
stream_name == camera or is_transcode_stream_name(camera, stream_name)
for camera in frigate_config.cameras
):
return True return True
config_streams = getattr(frigate_config.go2rtc, "streams", None) or {} config_streams = getattr(frigate_config.go2rtc, "streams", None) or {}
-6
View File
@@ -39,7 +39,6 @@ from frigate.util.image import create_mask
from frigate.util.live_streams import ( from frigate.util.live_streams import (
default_transcode_source, default_transcode_source,
is_transcode_stream_name, is_transcode_stream_name,
is_valid_go2rtc_stream_name,
transcode_stream_name, transcode_stream_name,
transcode_streams, transcode_streams,
) )
@@ -315,11 +314,6 @@ def apply_live_transcode_streams(
f"Camera {camera_config.name} has transcoded streams enabled, but its source {transcode.source} is not a go2rtc stream." f"Camera {camera_config.name} has transcoded streams enabled, but its source {transcode.source} is not a go2rtc stream."
) )
if not is_valid_go2rtc_stream_name(transcode.source):
raise ValueError(
f"Camera {camera_config.name} transcodes go2rtc stream {transcode.source}, whose name contains '#', whitespace or quotes."
)
generated = transcode_streams( generated = transcode_streams(
camera_config.name, camera_config.name,
transcode.source, transcode.source,
@@ -5,7 +5,6 @@ import tempfile
import unittest import unittest
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
import requests
import ruamel.yaml import ruamel.yaml
from fastapi import Request from fastapi import Request
@@ -235,72 +234,6 @@ class TestAuthInternalPort(BaseTestHttp):
finally: finally:
os.unlink(config_path) os.unlink(config_path)
@patch(
"frigate.api.auth.requests.get",
side_effect=requests.ConnectionError("go2rtc unavailable"),
)
def test_internal_port_rejects_unknown_go2rtc_src(self, _mock_get):
app = self._create_app()
with AuthTestClient(app) as client:
for url in (
"/live/mse/api/ws?src=exec:id",
"/live/webrtc/api/ws?src=ffmpeg:x%23raw=-f",
"/api/go2rtc/webrtc?src=front_door&src=unknown",
):
resp = client.get(
"/auth",
headers={"x-server-port": "5000", "x-original-url": url},
)
self.assertEqual(resp.status_code, 403, url)
def test_internal_port_allows_existing_go2rtc_stream(self):
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.get(
"/auth",
headers={
"x-server-port": "5000",
"x-original-url": "/live/mse/api/ws?src=front_door",
},
)
self.assertEqual(resp.status_code, 202)
self.assertEqual(resp.headers["remote-role"], "admin")
def test_internal_port_skips_go2rtc_check_for_other_paths(self):
app = self._create_app()
with AuthTestClient(app) as client:
for url in ("/api/stats", "/api/go2rtc/streams?src=unknown"):
resp = client.get(
"/auth",
headers={"x-server-port": "5000", "x-original-url": url},
)
self.assertEqual(resp.status_code, 202, url)
@patch(
"frigate.api.auth.requests.get",
side_effect=requests.ConnectionError("go2rtc unavailable"),
)
def test_live_stream_value_is_not_trusted_as_a_go2rtc_stream(self, _mock_get):
src = "ffmpeg:x#raw=-f"
self.minimal_config["cameras"]["front_door"]["live"] = {
"streams": {"Main": src}
}
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.get(
"/auth",
headers={
"x-server-port": "5000",
"x-original-url": "/live/mse/api/ws?src=ffmpeg:x%23raw=-f",
},
)
self.assertEqual(resp.status_code, 403)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)
@@ -151,19 +151,6 @@ class TestConfigGo2rtcStreamRestrictions(BaseTestHttp):
assert resp.status_code == 200, resp.text assert resp.status_code == 200, resp.text
assert _EXISTING_SRC in self._read_config() assert _EXISTING_SRC in self._read_config()
def test_set_rejects_stream_name_with_ffmpeg_params(self):
before = self._read_config()
resp = self._set_streams(
{"x#raw=-f#raw=mpegts#raw=/tmp/x": "rtsp://10.0.0.2:554/video"}
)
assert resp.status_code == 400
assert "Invalid go2rtc stream name" in resp.json()["message"]
assert self._read_config() == before
def test_set_allows_plain_stream_names(self):
resp = self._set_streams({"back-yard.main_2": "rtsp://10.0.0.2:554/video"})
assert resp.status_code == 200, resp.text
def test_set_allows_injected_source_when_override_set(self): def test_set_allows_injected_source_when_override_set(self):
with patch("frigate.util.services._GO2RTC_ARBITRARY_EXEC_ENV", "true"): with patch("frigate.util.services._GO2RTC_ARBITRARY_EXEC_ENV", "true"):
resp = self._set_streams({"new_stream": [_INJECTED_SRC]}) resp = self._set_streams({"new_stream": [_INJECTED_SRC]})
+14 -80
View File
@@ -194,16 +194,15 @@ class TestEnvVars(unittest.TestCase):
def setUp(self): def setUp(self):
self._original_env_vars = dict(FRIGATE_ENV_VARS) self._original_env_vars = dict(FRIGATE_ENV_VARS)
self._original_environ = os.environ.copy() self._original_environ = os.environ.copy()
names_patch = patch.object(env, "_CONTAINER_ENV_NAMES", frozenset())
names_patch.start()
self.addCleanup(names_patch.stop)
def tearDown(self): def tearDown(self):
FRIGATE_ENV_VARS.clear() FRIGATE_ENV_VARS.clear()
FRIGATE_ENV_VARS.update(self._original_env_vars) FRIGATE_ENV_VARS.update(self._original_env_vars)
env._CONFIG_ENV_VARS.clear() env._CONFIG_ENV_VARS.clear()
os.environ.clear() # Clean up any env vars we set
os.environ.update(self._original_environ) for key in list(os.environ.keys()):
if key not in self._original_environ:
del os.environ[key]
def _make_context(self, install: bool): def _make_context(self, install: bool):
"""Create a mock ValidationInfo with the given install flag.""" """Create a mock ValidationInfo with the given install flag."""
@@ -216,10 +215,10 @@ class TestEnvVars(unittest.TestCase):
return mock return mock
def test_install_sets_os_environ(self): def test_install_sets_os_environ(self):
"""validate_env_vars with install=True sets allowed names in os.environ.""" """validate_env_vars with install=True sets os.environ."""
ctx = self._make_context(install=True) ctx = self._make_context(install=True)
validate_env_vars({"SHM_MAX_FRAMES": "75"}, ctx) validate_env_vars({"MY_CUSTOM_VAR": "value123"}, ctx)
self.assertEqual(os.environ.get("SHM_MAX_FRAMES"), "75") self.assertEqual(os.environ.get("MY_CUSTOM_VAR"), "value123")
def test_install_updates_frigate_env_vars(self): def test_install_updates_frigate_env_vars(self):
"""validate_env_vars with install=True updates FRIGATE_ENV_VARS for FRIGATE_ keys.""" """validate_env_vars with install=True updates FRIGATE_ENV_VARS for FRIGATE_ keys."""
@@ -230,9 +229,9 @@ class TestEnvVars(unittest.TestCase):
def test_install_skips_non_frigate_in_env_vars_dict(self): def test_install_skips_non_frigate_in_env_vars_dict(self):
"""Non-FRIGATE_ keys are set in os.environ but not in FRIGATE_ENV_VARS.""" """Non-FRIGATE_ keys are set in os.environ but not in FRIGATE_ENV_VARS."""
ctx = self._make_context(install=True) ctx = self._make_context(install=True)
validate_env_vars({"USE_FP16": "False"}, ctx) validate_env_vars({"OTHER_VAR": "value"}, ctx)
self.assertEqual(os.environ.get("USE_FP16"), "False") self.assertEqual(os.environ.get("OTHER_VAR"), "value")
self.assertNotIn("USE_FP16", FRIGATE_ENV_VARS) self.assertNotIn("OTHER_VAR", FRIGATE_ENV_VARS)
def test_no_install_does_not_set(self): def test_no_install_does_not_set(self):
"""validate_env_vars without install=True does not modify state.""" """validate_env_vars without install=True does not modify state."""
@@ -259,10 +258,10 @@ class TestEnvVars(unittest.TestCase):
ctx = self._make_context(install=True) ctx = self._make_context(install=True)
with patch("frigate.util.services.os.path.isfile", return_value=False): with patch("frigate.util.services.os.path.isfile", return_value=False):
for key in ("GO2RTC_ALLOW_ARBITRARY_EXEC", "go2rtc_allow_arbitrary_exec"): for key in ("GO2RTC_ALLOW_ARBITRARY_EXEC", "go2rtc_allow_arbitrary_exec"):
validate_env_vars({key: "true", "USE_FP16": "False"}, ctx) validate_env_vars({key: "true", "OTHER_VAR": "value"}, ctx)
self.assertNotIn(key, os.environ) self.assertNotIn(key, os.environ)
self.assertFalse(is_go2rtc_arbitrary_exec_allowed()) self.assertFalse(is_go2rtc_arbitrary_exec_allowed())
self.assertEqual(os.environ.get("USE_FP16"), "False") self.assertEqual(os.environ.get("OTHER_VAR"), "value")
def test_install_keeps_container_go2rtc_exec_override(self): def test_install_keeps_container_go2rtc_exec_override(self):
"""An override set in the container environment is left untouched.""" """An override set in the container environment is left untouched."""
@@ -308,82 +307,17 @@ class TestVariableSources(unittest.TestCase):
self.assertEqual(env.FRIGATE_ENV_VARS["FRIGATE_CAM_PASS"], "hunter2") self.assertEqual(env.FRIGATE_ENV_VARS["FRIGATE_CAM_PASS"], "hunter2")
def test_config_env_vars_do_not_become_container_env(self): def test_config_env_vars_do_not_become_container_env(self):
"""environment_vars FRIGATE_ keys stay out of os.environ and env precedence.""" """environment_vars writes os.environ but must not gain env precedence."""
env.apply_config_env_vars({"FRIGATE_CAM_PASS": "from_config"}) env.apply_config_env_vars({"FRIGATE_CAM_PASS": "from_config"})
self.assertNotIn("FRIGATE_CAM_PASS", os.environ) self.assertEqual(os.environ["FRIGATE_CAM_PASS"], "from_config")
self.assertNotIn("FRIGATE_CAM_PASS", env._CONTAINER_ENV) self.assertNotIn("FRIGATE_CAM_PASS", env._CONTAINER_ENV)
def test_non_frigate_config_env_vars_only_set_os_environ(self): def test_non_frigate_config_env_vars_only_set_os_environ(self):
"""Unprefixed environment_vars keys reach os.environ but not substitution.""" """Unprefixed environment_vars keys reach os.environ but not substitution."""
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"}) env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "i965") self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "i965")
self.assertNotIn("LIBVA_DRIVER_NAME", env.FRIGATE_ENV_VARS) self.assertNotIn("LIBVA_DRIVER_NAME", env.FRIGATE_ENV_VARS)
def test_rejected_names_warn_and_are_not_exported(self):
"""Names off the allowlist log a warning without the value."""
for key in ("MY_CUSTOM_VAR", "LD_PRELOAD", "LIGHTER_ANE_EP", "PATH"):
with self.subTest(key=key):
original = os.environ.get(key)
with self.assertLogs("frigate.util.env", level="WARNING") as logs:
env.apply_config_env_vars({key: "/config/evil.so"})
self.assertEqual(os.environ.get(key), original)
self.assertEqual(len(logs.output), 1)
self.assertIn(key, logs.output[0])
self.assertIn("container environment", logs.output[0])
self.assertNotIn("evil", logs.output[0])
def test_allowed_name_with_bad_value_is_rejected(self):
"""LIBVA_DRIVER_NAME cannot carry a path."""
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
with self.assertLogs("frigate.util.env", level="WARNING"):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "../x"})
self.assertNotEqual(os.environ.get("LIBVA_DRIVER_NAME"), "../x")
def test_unsupported_values_are_rejected_without_crashing(self):
"""Non-ASCII digits and NUL bytes never reach os.environ."""
os.environ.pop("SHM_MAX_FRAMES", None)
os.environ.pop("HF_ENDPOINT", None)
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
with self.assertLogs("frigate.util.env", level="WARNING") as logs:
env.apply_config_env_vars(
{"SHM_MAX_FRAMES": "\u0663\u0664", "HF_ENDPOINT": "https://x\x00"}
)
self.assertEqual(len(logs.output), 2)
self.assertNotIn("SHM_MAX_FRAMES", os.environ)
self.assertNotIn("HF_ENDPOINT", os.environ)
def test_container_env_wins_over_allowed_name(self):
"""A name the container set is left as the container set it."""
os.environ["LIBVA_DRIVER_NAME"] = "iHD"
with patch.object(
env, "_CONTAINER_ENV_NAMES", frozenset({"LIBVA_DRIVER_NAME"})
):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "iHD")
def test_second_apply_is_not_blocked_by_first(self):
"""A safe-mode reload can change a value the first load exported."""
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "radeonsi"})
self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "radeonsi")
def test_run_as_root_is_substitutable_but_not_exported(self):
"""FRIGATE_RUN_AS_ROOT from the config never reaches os.environ."""
os.environ.pop("FRIGATE_RUN_AS_ROOT", None)
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"FRIGATE_RUN_AS_ROOT": "true"})
self.assertNotIn("FRIGATE_RUN_AS_ROOT", os.environ)
self.assertEqual(env.substitute_frigate_vars("{FRIGATE_RUN_AS_ROOT}"), "true")
def test_jwt_secret_is_exported(self):
"""Auth reads FRIGATE_JWT_SECRET from os.environ."""
os.environ.pop("FRIGATE_JWT_SECRET", None)
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"FRIGATE_JWT_SECRET": "s3cret"})
self.assertEqual(os.environ["FRIGATE_JWT_SECRET"], "s3cret")
def test_collision_warns_once_naming_the_winner(self): def test_collision_warns_once_naming_the_winner(self):
"""A key from two sources logs one warning naming the source that won.""" """A key from two sources logs one warning naming the source that won."""
with patch.dict(env._CONTAINER_ENV, {"FRIGATE_MQTT_HOST": "from_env"}): with patch.dict(env._CONTAINER_ENV, {"FRIGATE_MQTT_HOST": "from_env"}):
+6 -125
View File
@@ -1,15 +1,13 @@
"""Tests for custom ffmpeg path resolution and the root-mode guard.""" """Tests for custom ffmpeg path resolution and the root-mode guard."""
import os
import tempfile
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from frigate.const import DEFAULT_FFMPEG_VERSION from frigate.const import DEFAULT_FFMPEG_VERSION
from frigate.util.config import ( from frigate.util.config import (
_warn_ignored_ffmpeg_path, _warn_ignored_ffmpeg_path,
frigate_service_is_granular_root,
resolve_ffmpeg_path, resolve_ffmpeg_path,
service_is_granular_root,
) )
BUNDLED = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" BUNDLED = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
@@ -23,20 +21,12 @@ class TestConfigFfmpegRootGuard(unittest.TestCase):
# the warning is memoized so it fires once per path, not per camera # the warning is memoized so it fires once per path, not per camera
_warn_ignored_ffmpeg_path.cache_clear() _warn_ignored_ffmpeg_path.cache_clear()
def _resolve( def _resolve(self, path: str, *, euid: int, env: dict, binary: str = "ffmpeg"):
self,
path: str,
*,
euid: int,
env: dict,
binary: str = "ffmpeg",
service: str = "frigate",
):
with ( with (
patch("os.geteuid", return_value=euid), patch("os.geteuid", return_value=euid),
patch.dict("os.environ", env, clear=True), patch.dict("os.environ", env, clear=True),
): ):
return resolve_ffmpeg_path(path, binary, service) return resolve_ffmpeg_path(path, binary)
def test_custom_path_used_when_service_is_unprivileged(self) -> None: def test_custom_path_used_when_service_is_unprivileged(self) -> None:
self.assertEqual( self.assertEqual(
@@ -75,59 +65,6 @@ class TestConfigFfmpegRootGuard(unittest.TestCase):
f"{CUSTOM}/bin/ffmpeg", f"{CUSTOM}/bin/ffmpeg",
) )
def test_custom_path_ignored_for_go2rtc_when_go2rtc_is_a_root_service(
self,
) -> None:
self.assertEqual(
self._resolve(
CUSTOM,
euid=0,
env={"FRIGATE_ROOT_SERVICES": "go2rtc"},
service="go2rtc",
),
BUNDLED,
)
def test_go2rtc_keeps_custom_path_when_only_frigate_is_root(self) -> None:
self.assertEqual(
self._resolve(
CUSTOM,
euid=0,
env={"FRIGATE_ROOT_SERVICES": "frigate"},
service="go2rtc",
),
f"{CUSTOM}/bin/ffmpeg",
)
def test_go2rtc_custom_path_used_in_default_drop_mode(self) -> None:
# the run scripts resolve as root but probe as the service user
self.assertEqual(
self._resolve(CUSTOM, euid=0, env={}, service="go2rtc"),
f"{CUSTOM}/bin/ffmpeg",
)
def test_escape_hatch_keeps_go2rtc_custom_path(self) -> None:
self.assertEqual(
self._resolve(
CUSTOM,
euid=0,
env={"FRIGATE_RUN_AS_ROOT": "true", "FRIGATE_ROOT_SERVICES": "go2rtc"},
service="go2rtc",
),
f"{CUSTOM}/bin/ffmpeg",
)
def test_go2rtc_root_owned_path_is_left_alone(self) -> None:
self.assertEqual(
self._resolve(
"/opt/custom-ffmpeg",
euid=0,
env={"FRIGATE_ROOT_SERVICES": "go2rtc"},
service="go2rtc",
),
"/opt/custom-ffmpeg/bin/ffmpeg",
)
def test_root_services_has_no_effect_under_docker_user(self) -> None: def test_root_services_has_no_effect_under_docker_user(self) -> None:
# docker's own user: means the service never had root to keep, which is # docker's own user: means the service never had root to keep, which is
# also the case for get_ffmpeg_path.py in a --user container # also the case for get_ffmpeg_path.py in a --user container
@@ -178,65 +115,15 @@ class TestConfigFfmpegRootGuard(unittest.TestCase):
) )
class TestConfigFfmpegRootGuardSymlinks(unittest.TestCase): class TestFrigateServiceIsGranularRoot(unittest.TestCase):
"""A root service gets the resolved path, so a link cannot be retargeted."""
def setUp(self) -> None:
_warn_ignored_ffmpeg_path.cache_clear()
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
root = os.path.realpath(tmp.name)
self.writable = os.path.join(root, "config")
self.root_owned = os.path.join(root, "opt", "ffmpeg")
os.makedirs(self.writable)
os.makedirs(self.root_owned)
self.link = os.path.join(self.writable, "link")
dirs = patch("frigate.util.config.RUNTIME_USER_WRITABLE_DIRS", (self.writable,))
dirs.start()
self.addCleanup(dirs.stop)
def _resolve(self, env: dict, service: str = "frigate") -> str:
with (
patch("os.geteuid", return_value=0),
patch.dict("os.environ", env, clear=True),
):
return resolve_ffmpeg_path(self.link, "ffmpeg", service)
def test_link_to_a_root_owned_tree_resolves_to_its_target(self) -> None:
os.symlink(self.root_owned, self.link)
for service in ("frigate", "go2rtc"):
self.assertEqual(
self._resolve({"FRIGATE_ROOT_SERVICES": service}, service),
f"{self.root_owned}/bin/ffmpeg",
)
def test_link_to_a_writable_tree_is_refused(self) -> None:
evil = os.path.join(self.writable, "evil")
os.makedirs(evil)
os.symlink(evil, self.link)
self.assertEqual(self._resolve({"FRIGATE_ROOT_SERVICES": "frigate"}), BUNDLED)
def test_bin_linked_into_a_writable_tree_is_refused(self) -> None:
evil = os.path.join(self.writable, "evil")
os.makedirs(evil)
os.symlink(evil, os.path.join(self.root_owned, "bin"))
os.symlink(self.root_owned, self.link)
self.assertEqual(self._resolve({"FRIGATE_ROOT_SERVICES": "frigate"}), BUNDLED)
def test_unprivileged_service_keeps_the_path_as_written(self) -> None:
os.symlink(self.root_owned, self.link)
self.assertEqual(self._resolve({}), f"{self.link}/bin/ffmpeg")
class TestServiceIsGranularRoot(unittest.TestCase):
"""Root via FRIGATE_ROOT_SERVICES only, never via the escape hatch.""" """Root via FRIGATE_ROOT_SERVICES only, never via the escape hatch."""
def _check(self, *, euid: int, env: dict, service: str = "frigate") -> bool: def _check(self, *, euid: int, env: dict) -> bool:
with ( with (
patch("os.geteuid", return_value=euid), patch("os.geteuid", return_value=euid),
patch.dict("os.environ", env, clear=True), patch.dict("os.environ", env, clear=True),
): ):
return service_is_granular_root(service) return frigate_service_is_granular_root()
def test_false_without_any_root_signal(self) -> None: def test_false_without_any_root_signal(self) -> None:
self.assertFalse(self._check(euid=0, env={})) self.assertFalse(self._check(euid=0, env={}))
@@ -259,12 +146,6 @@ class TestServiceIsGranularRoot(unittest.TestCase):
self._check(euid=0, env={"FRIGATE_ROOT_SERVICES": "go2rtc,nginx"}) self._check(euid=0, env={"FRIGATE_ROOT_SERVICES": "go2rtc,nginx"})
) )
def test_matches_the_named_service(self) -> None:
env = {"FRIGATE_ROOT_SERVICES": "go2rtc"}
self.assertTrue(self._check(euid=0, env=env, service="go2rtc"))
self.assertFalse(self._check(euid=0, env=env, service="frigate"))
self.assertFalse(self._check(euid=1000, env=env, service="go2rtc"))
def test_substring_of_a_service_name_does_not_match(self) -> None: def test_substring_of_a_service_name_does_not_match(self) -> None:
self.assertFalse(self._check(euid=0, env={"FRIGATE_ROOT_SERVICES": "frigatee"})) self.assertFalse(self._check(euid=0, env={"FRIGATE_ROOT_SERVICES": "frigatee"}))
+1 -6
View File
@@ -23,12 +23,7 @@ _CONFIG = {
"dual_user": ["front_door", "back_door"], "dual_user": ["front_door", "back_door"],
} }
}, },
"go2rtc": { "go2rtc": {"streams": {"doorbell_audio": ["rtsp://10.0.0.4:554/audio"]}},
"streams": {
"doorbell_audio": ["rtsp://10.0.0.4:554/audio"],
"front_door_sub": ["rtsp://10.0.0.1:554/sub"],
}
},
"cameras": { "cameras": {
"front_door": { "front_door": {
"ffmpeg": { "ffmpeg": {
-35
View File
@@ -12,7 +12,6 @@ from frigate.util.live_streams import (
default_transcode_source, default_transcode_source,
generated_transcode_streams, generated_transcode_streams,
is_transcode_stream_name, is_transcode_stream_name,
is_valid_go2rtc_stream_name,
measure_stream_bitrate, measure_stream_bitrate,
raw_transcode_streams, raw_transcode_streams,
sync_transcode_streams, sync_transcode_streams,
@@ -38,13 +37,6 @@ class TestTranscodeNaming(unittest.TestCase):
"#raw=-b:v#raw=500k#raw=-maxrate#raw=500k#raw=-bufsize#raw=1000k", "#raw=-b:v#raw=500k#raw=-maxrate#raw=500k#raw=-bufsize#raw=1000k",
) )
def test_valid_go2rtc_stream_names(self):
for name in ("front", "front_sub", "back-yard.main_2", "Haustür"):
self.assertTrue(is_valid_go2rtc_stream_name(name), name)
for name in ("x#raw=-f", "front door", "front\tdoor", "a'b", 'a"b', "a${X}"):
self.assertFalse(is_valid_go2rtc_stream_name(name), name)
def test_default_source_skips_transcoded_streams(self): def test_default_source_skips_transcoded_streams(self):
streams = {"Low": "front_transcode_360p", "Main": "front_main"} streams = {"Low": "front_transcode_360p", "Main": "front_main"}
self.assertEqual(default_transcode_source("front", streams), "front_main") self.assertEqual(default_transcode_source("front", streams), "front_main")
@@ -129,22 +121,6 @@ class TestRawTranscodeStreams(unittest.TestCase):
self.assertEqual(list(raw_transcode_streams(config)), ["front_transcode_360p"]) self.assertEqual(list(raw_transcode_streams(config)), ["front_transcode_360p"])
def test_unsafe_source_name_is_skipped(self):
config = {
"cameras": {
"front": {
"live": {
"transcode": {"enabled": True, "source": "x#raw=-f#raw=null"}
}
}
}
}
self.assertEqual(raw_transcode_streams(config), {})
config["cameras"]["front"]["live"]["transcode"]["source"] = "front_main"
self.assertEqual(len(raw_transcode_streams(config)), 3)
def test_disabled_or_missing_generates_nothing(self): def test_disabled_or_missing_generates_nothing(self):
self.assertEqual(raw_transcode_streams({}), {}) self.assertEqual(raw_transcode_streams({}), {})
self.assertEqual( self.assertEqual(
@@ -310,17 +286,6 @@ class TestLiveTranscodeConfig(unittest.TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.streams({"transcode": {"enabled": True, "source": "missing"}}) self.streams({"transcode": {"enabled": True, "source": "missing"}})
def test_source_name_with_ffmpeg_params_fails(self):
name = "x#raw=-f#raw=null"
with self.assertRaisesRegex(ValidationError, "contains '#'"):
self.streams(
{"transcode": {"enabled": True, "source": name}},
go2rtc={
"front": ["rtsp://10.0.0.1:554/main"],
name: ["rtsp://10.0.0.1:554/main"],
},
)
def test_duplicate_heights_fail(self): def test_duplicate_heights_fail(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.streams( self.streams(
+15 -24
View File
@@ -46,14 +46,14 @@ DROPPED_DETECTOR_OPTIONS = {
} }
# Trees the unprivileged runtime user can write. A root service must not # Trees the unprivileged runtime user can write. A root frigate service must
# execute a binary from any of them; a compromised uid-1000 process could # not execute a binary from any of them; a compromised uid-1000 process could
# plant one and be root after the next restart. # plant one and be root after the next restart.
RUNTIME_USER_WRITABLE_DIRS = (CONFIG_DIR, BASE_DIR, CACHE_DIR, "/dev/shm", "/tmp") RUNTIME_USER_WRITABLE_DIRS = (CONFIG_DIR, BASE_DIR, CACHE_DIR, "/dev/shm", "/tmp")
def service_is_granular_root(service: str) -> bool: def frigate_service_is_granular_root() -> bool:
"""Report whether FRIGATE_ROOT_SERVICES runs the given service as root. """Report whether FRIGATE_ROOT_SERVICES runs frigate as root.
The escape hatch is excluded: it never sweeps /config and leaves no The escape hatch is excluded: it never sweeps /config and leaves no
unprivileged service running, so custom binaries stay as safe as they unprivileged service running, so custom binaries stay as safe as they
@@ -66,7 +66,7 @@ def service_is_granular_root(service: str) -> bool:
return False return False
entries = os.environ.get("FRIGATE_ROOT_SERVICES", "").split(",") entries = os.environ.get("FRIGATE_ROOT_SERVICES", "").split(",")
return any("".join(entry.split()) == service for entry in entries) return any("".join(entry.split()) == "frigate" for entry in entries)
def is_runtime_user_writable(path: str) -> bool: def is_runtime_user_writable(path: str) -> bool:
@@ -79,12 +79,11 @@ def is_runtime_user_writable(path: str) -> bool:
@cache @cache
def _warn_ignored_ffmpeg_path(path: str, service: str) -> None: def _warn_ignored_ffmpeg_path(path: str) -> None:
"""Warn once per path; resolution runs per camera and per binary.""" """Warn once per path; resolution runs per camera and per binary."""
logger.warning( logger.warning(
"Ignoring ffmpeg.path %s because FRIGATE_ROOT_SERVICES runs %s as root and that location is writable by the unprivileged user; using the bundled build", "Ignoring ffmpeg.path %s because FRIGATE_ROOT_SERVICES runs frigate as root and that location is writable by the unprivileged user; using the bundled build",
path, path,
service,
) )
@@ -98,19 +97,16 @@ def is_valid_ffmpeg_path(path: str) -> bool:
return not path.startswith("/") or _CUSTOM_FFMPEG_PATH.fullmatch(path) is not None return not path.startswith("/") or _CUSTOM_FFMPEG_PATH.fullmatch(path) is not None
def resolve_ffmpeg_path( def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str:
path: str, binary: str = "ffmpeg", service: str = "frigate"
) -> str:
"""Resolve an ffmpeg version alias or custom path to a binary path. """Resolve an ffmpeg version alias or custom path to a binary path.
A bare version alias that is no longer bundled (for example one that was A bare version alias that is no longer bundled (for example one that was
dropped when the default version changed) falls back to the default dropped when the default version changed) falls back to the default
bundled version so existing configs keep working across an upgrade or a bundled version so existing configs keep working across an upgrade or a
revert. Custom install paths (anything absolute) are used as-is. While revert. Custom install paths (anything absolute) are used as-is, unless
FRIGATE_ROOT_SERVICES makes the service that execs it root, a custom path they fail `is_valid_ffmpeg_path` (which falls back to the default), or
is resolved to its real location and refused inside a runtime-user-writable they are in a runtime-user-writable tree while FRIGATE_ROOT_SERVICES
tree; see RUNTIME_USER_WRITABLE_DIRS. Paths that fail makes frigate root; see RUNTIME_USER_WRITABLE_DIRS.
`is_valid_ffmpeg_path` also fall back to the default.
""" """
if ( if (
path == "default" path == "default"
@@ -120,16 +116,11 @@ def resolve_ffmpeg_path(
version = DEFAULT_FFMPEG_VERSION version = DEFAULT_FFMPEG_VERSION
elif path in INCLUDED_FFMPEG_VERSIONS: elif path in INCLUDED_FFMPEG_VERSIONS:
version = path version = path
elif not service_is_granular_root(service):
return f"{path}/bin/{binary}"
else: else:
# return the resolved form so a link in the path cannot be retargeted if not (frigate_service_is_granular_root() and is_runtime_user_writable(path)):
# after the check return f"{path}/bin/{binary}"
real = os.path.realpath(f"{path}/bin/{binary}")
if is_valid_ffmpeg_path(real) and not is_runtime_user_writable(real):
return real
_warn_ignored_ffmpeg_path(path, service) _warn_ignored_ffmpeg_path(path)
version = DEFAULT_FFMPEG_VERSION version = DEFAULT_FFMPEG_VERSION
return f"/usr/lib/ffmpeg/{version}/bin/{binary}" return f"/usr/lib/ffmpeg/{version}/bin/{binary}"
+15 -59
View File
@@ -26,8 +26,6 @@ _SECRETS_FILE: dict[str, str] = {}
_CONTAINER_ENV: dict[str, str] = { _CONTAINER_ENV: dict[str, str] = {
k: v for k, v in os.environ.items() if k.startswith("FRIGATE_") k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")
} }
# Empty image defaults, like ROCm's HSA_OVERRIDE_GFX_VERSION, don't count as set.
_CONTAINER_ENV_NAMES = frozenset(k for k, v in os.environ.items() if v)
_CREDENTIALS_DIR: dict[str, str] = {} _CREDENTIALS_DIR: dict[str, str] = {}
_SOURCES: tuple[tuple[str, dict[str, str]], ...] = ( _SOURCES: tuple[tuple[str, dict[str, str]], ...] = (
@@ -167,80 +165,38 @@ def reload_sources(warn: bool = True) -> None:
_rebuild(warn) _rebuild(warn)
_INT = re.compile(r"\d+", re.ASCII) # Security overrides that must come from the container environment, which is
_WORD = re.compile(r"\w+", re.ASCII) # controlled by the operator, rather than from the config, which admins can edit
_URL = re.compile(r"https?://\S+") _PROTECTED_ENV_VARS = frozenset({"GO2RTC_ALLOW_ARBITRARY_EXEC"})
# The only unprefixed names the config may export, with the values each accepts.
# Anything else, such as LD_PRELOAD, can load code into Frigate or its children.
_ALLOWED_ENV_VARS: dict[str, re.Pattern[str]] = {
"LIBVA_DRIVER_NAME": re.compile(r"[A-Za-z0-9_]+"),
"TF_INTRA_OP_PARALLELISM_THREADS": _INT,
"TF_INTER_OP_PARALLELISM_THREADS": _INT,
"TF_DATASET_THREAD_POOL_SIZE": _INT,
"SHM_MAX_FRAMES": _INT,
"USE_FP16": _WORD,
"HF_HUB_OFFLINE": _WORD,
"TRANSFORMERS_OFFLINE": _WORD,
"HSA_OVERRIDE_GFX_VERSION": re.compile(r"[0-9.]+"),
"HF_ENDPOINT": _URL,
"GITHUB_ENDPOINT": _URL,
"GITHUB_RAW_ENDPOINT": _URL,
"TF_KERAS_MOBILENET_V2_WEIGHTS_URL": _URL,
}
# Auth reads it from os.environ; every other FRIGATE_ name only feeds substitution.
_EXPORTED_FRIGATE_VARS = frozenset({"FRIGATE_JWT_SECRET"})
def apply_config_env_vars(values: Mapping[str, object]) -> None: def apply_config_env_vars(values: Mapping[str, object]) -> None:
"""Install the environment_vars block as the lowest priority source. """Install the environment_vars block as the lowest priority source.
FRIGATE_ keys feed substitution. Allowlisted unprefixed keys with valid Unprefixed keys only set os.environ. Protected security overrides are
values are exported to os.environ. A name the container already set is skipped.
never overwritten.
""" """
exports: dict[str, str] = {}
for key, value in values.items(): for key, value in values.items():
resolved = str(value) if key.upper() in _PROTECTED_ENV_VARS:
if key.startswith("FRIGATE_"):
_CONFIG_ENV_VARS[key] = resolved
continue
pattern = _ALLOWED_ENV_VARS.get(key)
if pattern is None:
logger.warning( logger.warning(
"Ignoring %s in environment_vars, it must be set in the container environment", "Ignoring %s in environment_vars, it must be set in the container environment",
key, key,
) )
continue continue
if not pattern.fullmatch(resolved): resolved = str(value)
logger.warning("Ignoring %s in environment_vars, unsupported value", key)
continue
exports[key] = resolved if key.startswith("FRIGATE_"):
_CONFIG_ENV_VARS[key] = resolved
else:
os.environ[key] = resolved
_rebuild() _rebuild()
for key in _EXPORTED_FRIGATE_VARS & values.keys(): # Export the winning value; auth reads FRIGATE_JWT_SECRET from os.environ.
exports[key] = FRIGATE_ENV_VARS[key] for key in values:
if key.startswith("FRIGATE_"):
for key, value in exports.items(): os.environ[key] = FRIGATE_ENV_VARS[key]
if key in _CONTAINER_ENV_NAMES:
logger.info(
"Ignoring %s in environment_vars, the container environment sets it",
key,
)
continue
try:
os.environ[key] = value
except ValueError:
logger.warning("Ignoring %s in environment_vars, unsupported value", key)
reload_sources(warn=False) reload_sources(warn=False)
+1 -13
View File
@@ -31,16 +31,6 @@ def is_transcode_stream_name(camera: str, name: str) -> bool:
return re.fullmatch(rf"{re.escape(camera)}_transcode_\d+p", name) is not None return re.fullmatch(rf"{re.escape(camera)}_transcode_\d+p", name) is not None
def is_valid_go2rtc_stream_name(name: str) -> bool:
"""Return whether a stream name is safe to splice into a go2rtc source.
go2rtc splits sources on `#` and ffmpeg commands on whitespace, and expands
`${VAR}` at load, so a name with any of these, or with quotes, could add its
own parameters.
"""
return re.search(r"[\s#$'\"]", name) is None
def transcode_stream_source(source: str, height: int, bitrate: int) -> str: def transcode_stream_source(source: str, height: int, bitrate: int) -> str:
"""Build the go2rtc ffmpeg source that transcodes a stream to H.264. """Build the go2rtc ffmpeg source that transcodes a stream to H.264.
@@ -111,9 +101,7 @@ def raw_transcode_streams(config: dict[str, Any]) -> dict[str, str]:
camera, live_streams or {camera: camera} camera, live_streams or {camera: camera}
) )
# an unsafe name fails Frigate's validation; skipping it here keeps if source is None:
# it out of go2rtc
if source is None or not is_valid_go2rtc_stream_name(str(source)):
continue continue
qualities = transcode.get("qualities") qualities = transcode.get("qualities")
+2 -2
View File
@@ -32,8 +32,8 @@ from pathlib import Path
from frigate.const import MODEL_CACHE_DIR from frigate.const import MODEL_CACHE_DIR
from frigate.util.config import ( from frigate.util.config import (
frigate_service_is_granular_root,
is_runtime_user_writable, is_runtime_user_writable,
service_is_granular_root,
) )
from frigate.util.file import FileLock from frigate.util.file import FileLock
@@ -163,7 +163,7 @@ def _usable_reason() -> str | None:
base = user_base() base = user_base()
if service_is_granular_root("frigate") and is_runtime_user_writable(str(base)): if frigate_service_is_granular_root() and is_runtime_user_writable(str(base)):
return ( return (
f"{base} is writable by the unprivileged user while " f"{base} is writable by the unprivileged user while "
"FRIGATE_ROOT_SERVICES runs frigate as root" "FRIGATE_ROOT_SERVICES runs frigate as root"
-7
View File
@@ -31,7 +31,6 @@ from frigate.const import (
) )
from frigate.util.builtin import clean_camera_user_pass, escape_special_characters from frigate.util.builtin import clean_camera_user_pass, escape_special_characters
from frigate.util.env import UnknownVariableError, substitute_frigate_vars from frigate.util.env import UnknownVariableError, substitute_frigate_vars
from frigate.util.live_streams import is_valid_go2rtc_stream_name
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -1178,7 +1177,6 @@ class Go2rtcStreamIssue(str, Enum):
restricted = "restricted" restricted = "restricted"
unknown_variable = "unknown_variable" unknown_variable = "unknown_variable"
invalid_placeholder = "invalid_placeholder" invalid_placeholder = "invalid_placeholder"
invalid_name = "invalid_name"
def find_restricted_go2rtc_stream_changes( def find_restricted_go2rtc_stream_changes(
@@ -1190,17 +1188,12 @@ def find_restricted_go2rtc_stream_changes(
in the saved config for the same stream is left alone, so streams the in the saved config for the same stream is left alone, so streams the
operator configured on disk keep working; any other source must resolve operator configured on disk keep working; any other source must resolve
its {FRIGATE_*} placeholders and pass `is_restricted_go2rtc_api_source`. its {FRIGATE_*} placeholders and pass `is_restricted_go2rtc_api_source`.
New stream names must pass `is_valid_go2rtc_stream_name`.
""" """
old_streams = old_streams if isinstance(old_streams, dict) else {} old_streams = old_streams if isinstance(old_streams, dict) else {}
new_streams = new_streams if isinstance(new_streams, dict) else {} new_streams = new_streams if isinstance(new_streams, dict) else {}
issues: dict[str, Go2rtcStreamIssue] = {} issues: dict[str, Go2rtcStreamIssue] = {}
for name, stream in new_streams.items(): for name, stream in new_streams.items():
if name not in old_streams and not is_valid_go2rtc_stream_name(str(name)):
issues[name] = Go2rtcStreamIssue.invalid_name
continue
existing = _go2rtc_stream_sources(old_streams.get(name)) existing = _go2rtc_stream_sources(old_streams.get(name))
for source in _go2rtc_stream_sources(stream): for source in _go2rtc_stream_sources(stream):
+169 -187
View File
@@ -9,8 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@cycjimmy/jsmpeg-player": "^7.0.0", "@cycjimmy/jsmpeg-player": "^6.1.8",
"@hookform/resolvers": "^7.0.3", "@hookform/resolvers": "^6.0.2",
"@radix-ui/react-alert-dialog": "^1.2.1", "@radix-ui/react-alert-dialog": "^1.2.1",
"@radix-ui/react-aspect-ratio": "^1.2.0", "@radix-ui/react-aspect-ratio": "^1.2.0",
"@radix-ui/react-checkbox": "^1.4.1", "@radix-ui/react-checkbox": "^1.4.1",
@@ -33,11 +33,11 @@
"@radix-ui/react-toggle": "^1.2.1", "@radix-ui/react-toggle": "^1.2.1",
"@radix-ui/react-toggle-group": "^1.2.1", "@radix-ui/react-toggle-group": "^1.2.1",
"@radix-ui/react-tooltip": "^1.4.1", "@radix-ui/react-tooltip": "^1.4.1",
"@rjsf/core": "^6.11.1", "@rjsf/core": "^6.10.1",
"@rjsf/shadcn": "^6.11.1", "@rjsf/shadcn": "^6.10.1",
"@rjsf/utils": "^6.10.1", "@rjsf/utils": "^6.10.1",
"@rjsf/validator-ajv8": "^6.11.1", "@rjsf/validator-ajv8": "^6.10.1",
"apexcharts": "^8.7.0", "apexcharts": "^8.6.0",
"axios": "^1.21.0", "axios": "^1.21.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -45,15 +45,15 @@
"copy-to-clipboard": "^4.0.2", "copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"framer-motion": "^14.9.1", "framer-motion": "^14.9.0",
"hls.js": "^1.7.4", "hls.js": "^1.7.4",
"i18next": "^26.7.1", "i18next": "^26.5.12",
"i18next-http-backend": "^4.0.10", "i18next-http-backend": "^4.0.7",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0", "js-yaml": "^5.5.0",
"konva": "^10.9.1", "konva": "^10.9.0",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"lucide-react": "^1.69.0", "lucide-react": "^1.65.0",
"monaco-yaml": "^5.5.1", "monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nosleep.js": "^0.12.0", "nosleep.js": "^0.12.0",
@@ -65,7 +65,7 @@
"react-dropzone": "^20.1.4", "react-dropzone": "^20.1.4",
"react-grid-layout": "^2.3.0", "react-grid-layout": "^2.3.0",
"react-hook-form": "^7.91.0", "react-hook-form": "^7.91.0",
"react-i18next": "^17.1.12", "react-i18next": "^17.1.9",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-konva": "^19.3.2", "react-konva": "^19.3.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -81,7 +81,7 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0", "use-long-press": "^3.3.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"virtua": "^0.54.2", "virtua": "^0.54.0",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7" "zod": "^4.6.7"
}, },
@@ -93,7 +93,7 @@
"@types/node": "^26.6.6", "@types/node": "^26.6.6",
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0", "@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.2.1", "@vitejs/plugin-react": "^6.1.2",
"autoprefixer": "^10.6.2", "autoprefixer": "^10.6.2",
"esbuild": "^0.28.2", "esbuild": "^0.28.2",
"eslint": "^10.14.0", "eslint": "^10.14.0",
@@ -101,7 +101,7 @@
"eslint-plugin-prettier": "^5.5.7", "eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.14.0", "globals": "^17.13.0",
"i18next-cli": "^1.5.11", "i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0", "monaco-editor": "^0.53.0",
"patch-package": "^8.0.1", "patch-package": "^8.0.1",
@@ -111,7 +111,7 @@
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.72.1", "typescript-eslint": "^8.72.1",
"vite": "^8.3.6" "vite": "^8.3.5"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@@ -428,9 +428,9 @@
} }
}, },
"node_modules/@cycjimmy/jsmpeg-player": { "node_modules/@cycjimmy/jsmpeg-player": {
"version": "7.0.0", "version": "6.1.8",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.8.tgz",
"integrity": "sha512-aPKrOKaon5ncMSN5ec2O6yqiIotPQr0KsSchIC4pnTnebA30bq4D3hKk4g4JynZzSIqBRWhd2TnyZh1vvTXxEA==", "integrity": "sha512-ccG7JvidQWWPvmmpu6Ob6TUnbyaz6WaTHl9V/5BD4vBVndxnxchLmVU3rpJguB2lWEoEAhFPBIU3GoeXtc3RaQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@date-fns/tz": { "node_modules/@date-fns/tz": {
@@ -1059,14 +1059,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "7.0.3", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-7.0.3.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.2.tgz",
"integrity": "sha512-beGY1JY6K5cyWlswD4BVf7UTb5MxSjKkjljrRyLPT3KIQzsTU0jOibBKqtdcKqIZYQUUyURed7roLWZtSdAtiQ==", "integrity": "sha512-e2dqUOlaGuirKVOd/iZWk/Yg5zeaGw7t0hSc3LzxL3o7eZGwZkTyEbBL8H52m0wRKEsNLWoMGOiXGHbPHTXlEQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@standard-schema/utils": "^0.3.0" "@standard-schema/utils": "^0.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@sinclair/typebox": ">=0.25.24",
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0",
"ajv": "^8.12.0", "ajv": "^8.12.0",
@@ -1086,13 +1087,15 @@
"react-hook-form": "^7.55.0", "react-hook-form": "^7.55.0",
"superstruct": ">=0.12.0", "superstruct": ">=0.12.0",
"typanion": "^3.3.2", "typanion": "^3.3.2",
"typebox": "^1.0.0",
"valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc",
"vest": ">=6.0.0", "vest": ">=6.0.0",
"yup": "^1.0.0", "yup": "^1.0.0",
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@sinclair/typebox": {
"optional": true
},
"@standard-schema/spec": { "@standard-schema/spec": {
"optional": true "optional": true
}, },
@@ -1147,9 +1150,6 @@
"typanion": { "typanion": {
"optional": true "optional": true
}, },
"typebox": {
"optional": true
},
"valibot": { "valibot": {
"optional": true "optional": true
}, },
@@ -1691,9 +1691,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.156.0", "version": "0.155.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
"integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -2903,47 +2903,47 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rjsf/core": { "node_modules/@rjsf/core": {
"version": "6.11.1", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.11.1.tgz", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.10.1.tgz",
"integrity": "sha512-2kH39lspUUQf552l+vSTj9vnwdv1gSOyDKqSDmFuTo16+92zbJ381knrQpk3KKowjp/sbxu4qPiBptgXH6i4UQ==", "integrity": "sha512-LHLbXYEq1+c2CpynBlf5JBlUhPNxOkVV+txf1BvamgJSt4QOisXFw5Uv+mBdLRi4BI+FfT0ofMVOgGWnuwG7gw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"markdown-to-jsx": "^9.10.3" "markdown-to-jsx": "^9.8.2"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
}, },
"peerDependencies": { "peerDependencies": {
"@rjsf/utils": "^6.11.1", "@rjsf/utils": "^6.10.1",
"react": ">=18" "react": ">=18"
} }
}, },
"node_modules/@rjsf/shadcn": { "node_modules/@rjsf/shadcn": {
"version": "6.11.1", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.11.1.tgz", "resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.10.1.tgz",
"integrity": "sha512-W00s4kLx5f4kerYtsE/kLLABqh3eg4Iih1D9llxKp0FDVnW6BEuJmKP8G9VRVphMdMQVkrZNeNBjlOE9Bc7ZDw==", "integrity": "sha512-URVLkYcf3V8hNo1ZvcwR9zdMx+2z2k5IhDpfmQqPqWrMvY469BL+Wgyp+P7PEF01JhlQA3GOB6EwfKTMTkpoAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.11", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.15", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-radio-group": "^1.4.7", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-separator": "^1.1.15", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.4.7", "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.3.3", "@radix-ui/react-slot": "^1.2.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"lucide-react": "^1.49.0", "lucide-react": "^1.16.0",
"tailwind-merge": "^3.7.0" "tailwind-merge": "^3.6.0"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
}, },
"peerDependencies": { "peerDependencies": {
"@rjsf/core": "^6.11.1", "@rjsf/core": "^6.10.1",
"@rjsf/utils": "^6.11.1", "@rjsf/utils": "^6.10.1",
"react": ">=18" "react": ">=18"
} }
}, },
@@ -2958,14 +2958,14 @@
} }
}, },
"node_modules/@rjsf/utils": { "node_modules/@rjsf/utils": {
"version": "6.11.1", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.11.1.tgz", "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.10.1.tgz",
"integrity": "sha512-rHQ4jNkhws3YUjvM/6RyW78l0mVfnt6w2j3/giUcS5F0cendCLYykFuSnE2fQxBwmknv4ANMOugwXOgcIbfvnA==", "integrity": "sha512-dlbQUCoTl/+/TenyYs1zAKXf9sLTebtY+6pB+bUFsaN4xkdAtR8YBLwmpa1N5yNiRzf3fbxveISNuZmDrZE/yQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@x0k/json-schema-merge": "^1.1.0", "@x0k/json-schema-merge": "^1.0.6",
"fast-equals": "^6.0.4", "fast-equals": "^6.0.0",
"fast-uri": "^4.2.1", "fast-uri": "^4.1.4",
"jsonpointer": "^5.0.1", "jsonpointer": "^5.0.1",
"react-is": "^18.3.1" "react-is": "^18.3.1"
}, },
@@ -2977,9 +2977,9 @@
} }
}, },
"node_modules/@rjsf/utils/node_modules/fast-equals": { "node_modules/@rjsf/utils/node_modules/fast-equals": {
"version": "6.2.2", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.2.2.tgz", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.0.tgz",
"integrity": "sha512-n+XqoarWCgcar0O3om3DeNYx6xTuLWGTUF1bRZ9iuO00tsPepzP1L4V7rqtxxZ4PEya34UCESReimFvxmEVwIA==", "integrity": "sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -3002,9 +3002,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@rjsf/validator-ajv8": { "node_modules/@rjsf/validator-ajv8": {
"version": "6.11.1", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.11.1.tgz", "resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.10.1.tgz",
"integrity": "sha512-LV7ibrep/kaRzkwzhLAmJY9SHOIZuX9fQO+0VCfm0FOGXneGU9xQEJvHft7R84uqtDTTQZqRq40HcQjqY0rsyw==", "integrity": "sha512-Jyim45UKd39SgWn4ryaydTigZ0dddNEeqZUfTlLgsy2v23uMDLyeKjNCItC09dy00dtKzwM3Q4WA0FtQ5IjOgA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"ajv": "^8.20.0", "ajv": "^8.20.0",
@@ -3014,13 +3014,13 @@
"node": ">=20" "node": ">=20"
}, },
"peerDependencies": { "peerDependencies": {
"@rjsf/utils": "^6.11.1" "@rjsf/utils": "^6.10.1"
} }
}, },
"node_modules/@rolldown/binding-android-arm-eabi": { "node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
"integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -3035,9 +3035,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
"integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3052,9 +3052,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
"integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3069,9 +3069,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
"integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3086,9 +3086,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
"integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3103,9 +3103,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
"integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -3120,16 +3120,13 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
"integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3140,16 +3137,13 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
"integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3160,16 +3154,13 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
"integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3180,16 +3171,13 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
"integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3200,16 +3188,13 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
"integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3220,16 +3205,13 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
"integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -3240,9 +3222,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
"integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3257,9 +3239,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
"integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -3274,9 +3256,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
"integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3911,9 +3893,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "6.2.1", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.2.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz",
"integrity": "sha512-9KyWCij3r0+qLG3Q62vBmv0VyevmYibAtxaCIVK9o0k6CXgaajZRIe8tpfchQcdZArJ5b/7OrIz4W3b2uxVRDA==", "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3923,9 +3905,9 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"peerDependencies": { "peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0 || ^0.3.0", "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"oxc-transform-react": ">=0.152.0 <0.157.0", "oxc-transform-react": "^0.152.0",
"vite": "^8.0.0" "vite": "^8.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -4081,9 +4063,9 @@
"license": "SEE LICENSE IN LICENSE.md" "license": "SEE LICENSE IN LICENSE.md"
}, },
"node_modules/apexcharts": { "node_modules/apexcharts": {
"version": "8.7.0", "version": "8.6.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.7.0.tgz", "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.6.0.tgz",
"integrity": "sha512-HHpMlM4QvLXFLL6hyn1iivF+3IyoCrBEXYcV9elJuT+Dc4At9P5+RAaSYA1onOrcXg59+VTAsnyUDdm4GRhUfA==", "integrity": "sha512-5524MLoDei2n8EcVMK+u3eOZTWrTpT1BIPRWu4AK8kkJdkODWs1hp3IEax8pMz78sSNAtsD4Tp7tr0s0SW/npQ==",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"dependencies": { "dependencies": {
"apex-commons": "^0.8.3" "apex-commons": "^0.8.3"
@@ -5596,12 +5578,12 @@
} }
}, },
"node_modules/framer-motion": { "node_modules/framer-motion": {
"version": "14.9.1", "version": "14.9.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.9.1.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.9.0.tgz",
"integrity": "sha512-WDWNT2ixHdmpvGg02BZWI/SWwRYzPiMUf4aIkTBgrxxD8RYFdZO3fCM4pnLMo6j8mUpj0+BuJRHQBChI4v3Anw==", "integrity": "sha512-g1hZu8WoTwgsoVAaqEEcefhvR03cKGtkmrKaRrWpXhnM3LGR8jl1u2qyJ1PMVlO7LKQHE8TTRQYUlMmnrY4L+g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"motion-dom": "14.9.1", "motion-dom": "14.9.0",
"motion-utils": "14.7.0", "motion-utils": "14.7.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
@@ -5804,9 +5786,9 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "17.14.0", "version": "17.13.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
"integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==", "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -6009,9 +5991,9 @@
} }
}, },
"node_modules/i18next": { "node_modules/i18next": {
"version": "26.7.1", "version": "26.5.12",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.7.1.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.12.tgz",
"integrity": "sha512-cX+3RRVczel/V2kCMTACckSGSpqgtWYEA6beXNYVms7dOQiHojdPYHD26lT4IKimNeoGWrLvtPgjftbRvmZJVw==", "integrity": "sha512-vTt8tc3y5axoU7wwTYg8vjpILLyP33a5GyGJAVI7FDRx1XueYxZVIkcVeStoftrVnAM/o28hKvK0sslucDI9aQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -6124,9 +6106,9 @@
} }
}, },
"node_modules/i18next-http-backend": { "node_modules/i18next-http-backend": {
"version": "4.0.10", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.10.tgz", "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.7.tgz",
"integrity": "sha512-CfdQuwIVijbrS/fmiTAQYcYyje4nMbH9aIjKYhhm5bgVmWKWLOM1F7qrA1o63Jl1JxQcRjdtWhKlvDzyqV9aPg==", "integrity": "sha512-SQy3OPy66YzumNJ43boHDBtzzMISnoPhQ0Zqjetk+luW74Tok8Ez9ujJJbJHKSHkG5sjDGldHznjuPtSxBwvuw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -6603,9 +6585,9 @@
} }
}, },
"node_modules/konva": { "node_modules/konva": {
"version": "10.9.1", "version": "10.9.0",
"resolved": "https://registry.npmjs.org/konva/-/konva-10.9.1.tgz", "resolved": "https://registry.npmjs.org/konva/-/konva-10.9.0.tgz",
"integrity": "sha512-m49x+GatSxEUSvqahYhxpTjgKbtrLtw4P4o4B4iux953ZFpceaK52Ea+CFsAsd7W4I0Y5u6mMoRUsY/eltNZmw==", "integrity": "sha512-3NOOlcbimFDp4+/DfXGTBkfn3cn3VP1BESPySQ6ayCzgMA9JSMGJNNxeIKw5jpHL6SAl6gHsrv10DUEjNRL0Qg==",
"funding": [ "funding": [
{ {
"type": "patreon", "type": "patreon",
@@ -6996,9 +6978,9 @@
} }
}, },
"node_modules/lucide-react": { "node_modules/lucide-react": {
"version": "1.69.0", "version": "1.65.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.69.0.tgz", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.65.0.tgz",
"integrity": "sha512-UTFECm4yEgkgMN0kfLuxWcmlk+4u02Jrqehgk3ZXRx/Ud68OpP7hjkp67bvkBHmIpMaZCIveF1QDYeupO7XmYQ==", "integrity": "sha512-roSdtzudEY39i2hn5LLjA8toYfhF2z4S10WSqZF4ubXpHlOmNTvaxMxZLdJfQDi6pEifQWX+cRmYihKSsvAxfA==",
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@@ -7021,9 +7003,9 @@
} }
}, },
"node_modules/markdown-to-jsx": { "node_modules/markdown-to-jsx": {
"version": "9.10.5", "version": "9.10.2",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.10.5.tgz", "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.10.2.tgz",
"integrity": "sha512-m/OFdkxlmWY13o3R0Yy6STooQRzJ0Q5IqqDqQeHOb7pAA2LNgLplEI+jhDyMdxpdr1pceFH6ykrBWGD6oRvW/A==", "integrity": "sha512-iR9GadlIox0q1uXnpqdxpF02Vb1WDmZ/QIXWjBR5htzjUEEhyIWbM65LuVKavdwJaVo4q95C/F2OOyNjWe91ig==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
@@ -8073,9 +8055,9 @@
} }
}, },
"node_modules/motion-dom": { "node_modules/motion-dom": {
"version": "14.9.1", "version": "14.9.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.9.1.tgz", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.9.0.tgz",
"integrity": "sha512-2eCCGXH2Yv09lM8GHK3TdfChrcvHxzVY19LD9VraoHEaQl5+PuGnTNnmJaBAR+5MdxUVdsLFXygFJjR9RwRSxw==", "integrity": "sha512-wGjKbNDMHd75aLStkxLCKZ+T8/wusZHklmNLK1Ak68UrdBt2X8I4yweYR/0WonxGnBsw6oloDTIV8pN+UTvzzA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"motion-utils": "14.7.0" "motion-utils": "14.7.0"
@@ -9077,9 +9059,9 @@
} }
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "17.1.12", "version": "17.1.9",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.12.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.9.tgz",
"integrity": "sha512-Gf8riCKF/EmqQ8I1g0N5pJOLvJCkAXGAEkzOuRNHc/sBGL3dfE3WZ7+sDLrUo/kw/xdug3g48jo7M+s/SKf0HQ==", "integrity": "sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.29.7", "@babel/runtime": "^7.29.7",
@@ -9495,13 +9477,13 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.2.16", "version": "1.2.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
"integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.156.0", "@oxc-project/types": "=0.155.0",
"@rolldown/pluginutils": "^1.0.0" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
@@ -9511,21 +9493,21 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.16", "@rolldown/binding-android-arm-eabi": "1.2.15",
"@rolldown/binding-android-arm64": "1.2.16", "@rolldown/binding-android-arm64": "1.2.15",
"@rolldown/binding-darwin-arm64": "1.2.16", "@rolldown/binding-darwin-arm64": "1.2.15",
"@rolldown/binding-darwin-x64": "1.2.16", "@rolldown/binding-darwin-x64": "1.2.15",
"@rolldown/binding-freebsd-x64": "1.2.16", "@rolldown/binding-freebsd-x64": "1.2.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.16", "@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
"@rolldown/binding-linux-arm64-gnu": "1.2.16", "@rolldown/binding-linux-arm64-gnu": "1.2.15",
"@rolldown/binding-linux-arm64-musl": "1.2.16", "@rolldown/binding-linux-arm64-musl": "1.2.15",
"@rolldown/binding-linux-ppc64-gnu": "1.2.16", "@rolldown/binding-linux-ppc64-gnu": "1.2.15",
"@rolldown/binding-linux-s390x-gnu": "1.2.16", "@rolldown/binding-linux-s390x-gnu": "1.2.15",
"@rolldown/binding-linux-x64-gnu": "1.2.16", "@rolldown/binding-linux-x64-gnu": "1.2.15",
"@rolldown/binding-linux-x64-musl": "1.2.16", "@rolldown/binding-linux-x64-musl": "1.2.15",
"@rolldown/binding-openharmony-arm64": "1.2.16", "@rolldown/binding-openharmony-arm64": "1.2.15",
"@rolldown/binding-win32-arm64-msvc": "1.2.16", "@rolldown/binding-win32-arm64-msvc": "1.2.15",
"@rolldown/binding-win32-x64-msvc": "1.2.16" "@rolldown/binding-win32-x64-msvc": "1.2.15"
} }
}, },
"node_modules/run-async": { "node_modules/run-async": {
@@ -10382,9 +10364,9 @@
} }
}, },
"node_modules/virtua": { "node_modules/virtua": {
"version": "0.54.2", "version": "0.54.0",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.2.tgz", "resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.0.tgz",
"integrity": "sha512-lYCAxyXCGkhXVvuKv8a+LklZquy0E9dIObYcOUyo6nP4VSvLeiIsiz94mA3ZKYuM5egkke5RIi2nXi5OIATc7A==", "integrity": "sha512-8nGGrDhX61MDVeqgyIaWrJqhRm9z2Nqa1WDqt0tk1m+ZWrvLesMP0iL0vL6xNsLCF2jE2ohwY+PqufdbVmRgEw==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=20.0.0", "@angular/common": ">=20.0.0",
@@ -10420,16 +10402,16 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.3.6", "version": "8.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.33.0", "lightningcss": "^1.33.0",
"picomatch": "^4.0.7", "picomatch": "^4.0.7",
"postcss": "^8.5.32", "postcss": "^8.5.30",
"rolldown": "~1.2.16", "rolldown": "~1.2.14",
"tinyglobby": "^0.2.17" "tinyglobby": "^0.2.17"
}, },
"bin": { "bin": {
+16 -16
View File
@@ -21,8 +21,8 @@
"i18n:status": "i18next-cli status" "i18n:status": "i18next-cli status"
}, },
"dependencies": { "dependencies": {
"@cycjimmy/jsmpeg-player": "^7.0.0", "@cycjimmy/jsmpeg-player": "^6.1.8",
"@hookform/resolvers": "^7.0.3", "@hookform/resolvers": "^6.0.2",
"@radix-ui/react-alert-dialog": "^1.2.1", "@radix-ui/react-alert-dialog": "^1.2.1",
"@radix-ui/react-aspect-ratio": "^1.2.0", "@radix-ui/react-aspect-ratio": "^1.2.0",
"@radix-ui/react-checkbox": "^1.4.1", "@radix-ui/react-checkbox": "^1.4.1",
@@ -45,11 +45,11 @@
"@radix-ui/react-toggle": "^1.2.1", "@radix-ui/react-toggle": "^1.2.1",
"@radix-ui/react-toggle-group": "^1.2.1", "@radix-ui/react-toggle-group": "^1.2.1",
"@radix-ui/react-tooltip": "^1.4.1", "@radix-ui/react-tooltip": "^1.4.1",
"@rjsf/core": "^6.11.1", "@rjsf/core": "^6.10.1",
"@rjsf/shadcn": "^6.11.1", "@rjsf/shadcn": "^6.10.1",
"@rjsf/utils": "^6.10.1", "@rjsf/utils": "^6.10.1",
"@rjsf/validator-ajv8": "^6.11.1", "@rjsf/validator-ajv8": "^6.10.1",
"apexcharts": "^8.7.0", "apexcharts": "^8.6.0",
"axios": "^1.21.0", "axios": "^1.21.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -57,15 +57,15 @@
"copy-to-clipboard": "^4.0.2", "copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0", "date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"framer-motion": "^14.9.1", "framer-motion": "^14.9.0",
"hls.js": "^1.7.4", "hls.js": "^1.7.4",
"i18next": "^26.7.1", "i18next": "^26.5.12",
"i18next-http-backend": "^4.0.10", "i18next-http-backend": "^4.0.7",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0", "js-yaml": "^5.5.0",
"konva": "^10.9.1", "konva": "^10.9.0",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"lucide-react": "^1.69.0", "lucide-react": "^1.65.0",
"monaco-yaml": "^5.5.1", "monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nosleep.js": "^0.12.0", "nosleep.js": "^0.12.0",
@@ -77,7 +77,7 @@
"react-dropzone": "^20.1.4", "react-dropzone": "^20.1.4",
"react-grid-layout": "^2.3.0", "react-grid-layout": "^2.3.0",
"react-hook-form": "^7.91.0", "react-hook-form": "^7.91.0",
"react-i18next": "^17.1.12", "react-i18next": "^17.1.9",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-konva": "^19.3.2", "react-konva": "^19.3.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -93,7 +93,7 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0", "use-long-press": "^3.3.0",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"virtua": "^0.54.2", "virtua": "^0.54.0",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7" "zod": "^4.6.7"
}, },
@@ -105,7 +105,7 @@
"@types/node": "^26.6.6", "@types/node": "^26.6.6",
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0", "@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.2.1", "@vitejs/plugin-react": "^6.1.2",
"autoprefixer": "^10.6.2", "autoprefixer": "^10.6.2",
"esbuild": "^0.28.2", "esbuild": "^0.28.2",
"eslint": "^10.14.0", "eslint": "^10.14.0",
@@ -113,7 +113,7 @@
"eslint-plugin-prettier": "^5.5.7", "eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.14.0", "globals": "^17.13.0",
"i18next-cli": "^1.5.11", "i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0", "monaco-editor": "^0.53.0",
"patch-package": "^8.0.1", "patch-package": "^8.0.1",
@@ -123,7 +123,7 @@
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.72.1", "typescript-eslint": "^8.72.1",
"vite": "^8.3.6" "vite": "^8.3.5"
}, },
"overrides": { "overrides": {
"@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2",
@@ -169,7 +169,7 @@ export function GenAIModelWidget(props: WidgetProps) {
if (previous === formProvider) return; if (previous === formProvider) return;
if (formProvider === savedProvider && value === savedModel) return; if (formProvider === savedProvider && value === savedModel) return;
if (typeof value === "string" && value) onChange(undefined); if (typeof value === "string" && value) onChange("");
}, [formProvider, savedProvider, savedModel, value, onChange]); }, [formProvider, savedProvider, savedModel, value, onChange]);
const probe = async () => { const probe = async () => {