Compare commits

..
7 Commits
Author SHA1 Message Date
ishan-karmakar d7719d6f39 Remove symlinks because Portainer doesn't like symlinks
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
i18n sync check / check:sync (push) Has been cancelled
2026-06-06 12:49:25 -05:00
ishan-karmakar 36e81f4851 Update docker-compose for our purposes
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-06-06 11:35:48 -05:00
sriram veeraghantaandGitHub 0bbfe95cc7 fix: bump react-router and vitest to resolve Dependabot advisories (#9215)
* fix: bump react-router and vitest to resolve Dependabot advisories

Resolves 6 open Dependabot alerts (all npm, manifest pnpm-lock.yaml):

- react-router 7.12.0 -> 7.15.0 (fixes GHSA-8x6r-g9mw-2r78 [high],
  GHSA-49rj-9fvp-4h2h [high], GHSA-8646-j5j9-6r62 [high],
  GHSA-2j2x-hqr9-3h42 [medium], GHSA-f22v-gfqf-p8f3 [medium])
- vitest 4.0.x -> 4.1.x (fixes GHSA-5xrq-8626-4rwp [critical])

Aligned lockstep siblings to avoid peer-dependency mismatches:
@react-router/dev|node|serve -> 7.15.0, @vitest/coverage-v8 -> ^4.1.0.

Edited catalog entries in pnpm-workspace.yaml and regenerated
pnpm-lock.yaml; verified with pnpm install --frozen-lockfile.

* fix: raise vitest catalog floor to ^4.1.8 to match security advisory

The critical advisory GHSA-5xrq-8626-4rwp is patched in vitest 4.1.8, but
the catalog specifiers were ^4.1.0, which permits resolving to vulnerable
4.1.0-4.1.7. Align the floor with the documented patched version for vitest
and @vitest/coverage-v8 so a future lockfile refresh cannot reintroduce a
vulnerable Vitest stack. Resolved version is unchanged (4.1.8).
2026-06-05 00:51:33 +05:30
sriram veeraghantaandGitHub 9a30a07cf5 fix(api): enforce workspace membership on GenericAssetEndpoint (#9212)
The public REST API GenericAssetEndpoint (/api/v1/workspaces/<slug>/assets/)
declared no permission class, inheriting only IsAuthenticated. Since
APIKeyAuthentication does not bind a token to a workspace and the workspace is
read straight from the URL slug, any valid Personal Access Token could read
(GET), create (POST), and modify (PATCH) assets in a workspace the caller is
not a member of — a cross-workspace IDOR, the public-API sibling of the
CVE-2026-46558 dashboard asset fix.

Add permission_classes = [WorkspaceUserPermission] so every method requires
active workspace membership, matching the dashboard fix semantics. Also add
contract regression tests covering cross-workspace GET/POST/PATCH (now 403)
and a positive control confirming members retain access.

Also ignore the local /security/ advisory notes folder.
2026-06-04 18:49:39 +05:30
Karthikeyan GaneshandGitHub b6e47ccdae fix: dropdown shadow on the work item more options (#9154)
* fix: UI border and shadow on the dropdown menu usability

* fix: shadow-md and border strong
2026-06-03 17:28:19 +05:30
Durgesh ShekhawatandGitHub 4280c4d1b1 fix: handle error message for special characters in Identifier of Project (#9059) 2026-06-03 17:26:18 +05:30
sriram veeraghantaandGitHub b1c78fe4c8 fix(api): rate-limit magic-code verify, bound per-token attempts (GHSA-9pvm-fcf6-9234) (#9130)
* fix(api): rate-limit magic-code verification and bound per-token attempts

The magic-link sign-in / sign-up endpoints accept a 6-digit numeric code
(900k-value space, 600s TTL) but never increment a failure counter on a
wrong-code verify and extend django.views.View rather than DRF APIView,
so DRF's AuthenticationThrottle never runs against them. The space-side
generate endpoint also lacked throttle_classes. Combined, this allowed
an unauthenticated attacker who knew a victim's email to brute-force
the code within the TTL window and log in as the victim.

- Add MAX_VERIFY_ATTEMPTS=5 in MagicCodeProvider.set_user_data: failed
  comparisons now persist verify_attempts in Redis under the remaining
  TTL and, on hitting the limit, delete the key and raise
  EMAIL_CODE_ATTEMPT_EXHAUSTED. This is the load-bearing fix - it caps
  total attempts per issued token regardless of request rate.
- Add authentication_throttle_allows() so plain Django Views can apply
  AuthenticationThrottle without converting to APIView (would change
  CSRF + request-parsing semantics for the redirect-flow endpoints).
- Apply the throttle to MagicSignIn/UpEndpoint and the space variants;
  add throttle_classes to MagicGenerateSpaceEndpoint to match its app
  sibling.

Refs GHSA-9pvm-fcf6-9234.

* fix(api): make verify-attempt increment atomic, expose throttle rate via env

Address PR review feedback:

- Replace the JSON read-modify-write of verify_attempts with a Lua
  EVAL script that INCRs a dedicated counter key and EXPIREs it only
  on the first increment. The previous round-trip was racy: parallel
  wrong-code requests could read the same value and both write the
  same incremented count, letting an attacker exceed MAX_VERIFY_ATTEMPTS
  under concurrency. Counter is now reset on each new token issuance
  and cleared on successful verify / exhaustion.
- Make AuthenticationThrottle.rate configurable via the
  AUTHENTICATION_RATE_LIMIT env var (default 10/minute, down from 30
  to tighten the budget on unauth auth-adjacent endpoints). Document
  it in deployments/aio and deployments/cli variables.env.

* test(api): cover magic-code attempt cap, counter reset, and auth throttle

Add the contract tests called out in the PR test plan:

- TestMagicSignInVerifyAttempts:
  - test_exhausted_after_max_wrong_attempts: after MAX_VERIFY_ATTEMPTS
    wrong codes the next verify redirects with EMAIL_CODE_ATTEMPT_
    EXHAUSTED_SIGN_IN and both Redis keys are deleted; a follow-up
    verify reports EXPIRED.
  - test_counter_increments_on_each_wrong_attempt: the dedicated
    verify_attempts counter advances by exactly one per wrong POST,
    matching the atomic Lua INCR.
  - test_counter_resets_on_token_regeneration: regenerating the
    magic-link clears the counter so the user isn't pre-locked-out by
    a prior session's wrong attempts.
- TestMagicSignUpVerifyAttempts.test_signup_exhausted_after_max_wrong_attempts:
  the sign-up endpoint returns EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP on
  the exhausting attempt.
- TestAuthenticationThrottle: exercises authentication_throttle_allows
  on the plain-View redirect-flow endpoints by patching the rate down
  and asserting RATE_LIMIT_EXCEEDED is appended to the redirect URL
  once the per-IP budget is exceeded, for both magic-sign-in and
  magic-sign-up.

Each new class clears Django cache (DRF throttle storage) and the
per-email Redis keys around every test so runs are independent.

* fix(api): clamp remaining_ttl to >=1 for verify-attempt counter EXPIRE

ri.ttl() returns 0 when the token has less than one second remaining
(Redis floors to whole seconds). The previous clamp only caught
None and < 0, so a sub-second TTL would pass through and the Lua
script's EXPIRE counter 0 would immediately delete the key — letting
an attacker bypass MAX_VERIFY_ATTEMPTS during the final second of the
token's life. Switch the comparison to <= 0.

Narrow real-world impact (sub-second window, throttle still bounds
the rate) but the cap should hold regardless of timing.
2026-06-01 18:44:57 +05:30
17 changed files with 717 additions and 257 deletions
+2 -2
View File
@@ -115,5 +115,5 @@ scripts/
# i18n auto-generated types (regenerated on every build)
packages/i18n/src/types/keys.generated.ts
# Local security advisory notes (not for version control)
/advisories.md
# Local security notes (not for version control)
/security/
+7
View File
@@ -19,6 +19,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from plane.settings.storage import S3Storage
from plane.utils.path_validator import sanitize_filename
from plane.db.models import FileAsset, User, Workspace
from plane.app.permissions import WorkspaceUserPermission
from plane.api.views.base import BaseAPIView
from plane.api.serializers import (
UserAssetUploadSerializer,
@@ -404,6 +405,12 @@ class UserServerAssetEndpoint(BaseAPIView):
class GenericAssetEndpoint(BaseAPIView):
"""This endpoint is used to upload generic assets that can be later bound to entities."""
# The workspace is taken straight from the URL slug, so every method must
# verify the caller is an active member of that workspace. Without this the
# endpoint is a cross-workspace IDOR (the public-API sibling of the
# CVE-2026-46558 dashboard fix).
permission_classes = [WorkspaceUserPermission]
use_read_replica = True
@asset_docs(
@@ -22,6 +22,27 @@ from plane.db.models import User
class MagicCodeProvider(CredentialAdapter):
provider = "magic-code"
# Max wrong-code verification attempts per issued token before the token
# is invalidated. Prevents brute-forcing the 6-digit code space within
# the token TTL window.
MAX_VERIFY_ATTEMPTS = 5
# Atomic INCR + first-time EXPIRE for the verify-attempt counter.
# Using a dedicated counter key with this script makes the increment
# safe under concurrent wrong-code requests; a plain JSON read/modify/
# write would race and let parallel attackers exceed the cap.
_INCREMENT_VERIFY_ATTEMPTS_SCRIPT = (
'local count = redis.call("INCR", KEYS[1]) '
'if count == 1 then '
' redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1])) '
'end '
'return count'
)
@staticmethod
def _verify_attempts_key(token_key):
return f"{token_key}:verify_attempts"
def __init__(self, request, key, code=None, callback=None):
(EMAIL_HOST, ENABLE_MAGIC_LINK_LOGIN) = get_configuration_value(
[
@@ -92,6 +113,9 @@ class MagicCodeProvider(CredentialAdapter):
expiry = 600
ri.set(key, json.dumps(value), ex=expiry)
# Reset the verify-attempt counter so each newly issued token starts
# with a fresh budget of MAX_VERIFY_ATTEMPTS.
ri.delete(self._verify_attempts_key(key))
return key, token
def set_user_data(self):
@@ -114,12 +138,52 @@ class MagicCodeProvider(CredentialAdapter):
},
}
)
# Delete the token from redis if the code match is successful
# Delete the token and its counter from redis on success.
ri.delete(self.key)
ri.delete(self._verify_attempts_key(self.key))
return
else:
email = str(self.key).replace("magic_", "", 1)
if User.objects.filter(email=email).exists():
user_exists = User.objects.filter(email=email).exists()
# Atomically increment the verify-attempt counter in Redis.
# The Lua script sets the TTL only on the first increment so
# the lockout window matches the remaining token TTL and does
# not get extended by every wrong-code attempt.
# ri.ttl() returns -2 (missing), -1 (no expiry), 0 (sub-second
# remaining; Redis floors to whole seconds), or a positive int.
# Clamp to >=1 because EXPIRE key 0 immediately deletes the key
# and would let an attacker bypass the cap in the final second.
remaining_ttl = ri.ttl(self.key)
if remaining_ttl is None or remaining_ttl <= 0:
remaining_ttl = 1
verify_attempts = int(
ri.eval(
self._INCREMENT_VERIFY_ATTEMPTS_SCRIPT,
1,
self._verify_attempts_key(self.key),
remaining_ttl,
)
)
if verify_attempts >= self.MAX_VERIFY_ATTEMPTS:
# Invalidate the token (and counter) so further attempts
# must regenerate; regeneration is itself attempt-counted.
ri.delete(self.key)
ri.delete(self._verify_attempts_key(self.key))
if user_exists:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN"],
error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN",
payload={"email": str(email)},
)
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP"],
error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP",
payload={"email": str(email)},
)
if user_exists:
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_MAGIC_CODE_SIGN_IN"],
error_message="INVALID_MAGIC_CODE_SIGN_IN",
+22 -1
View File
@@ -2,6 +2,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import os
# Third party imports
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from rest_framework import status
@@ -15,7 +18,9 @@ from plane.authentication.adapter.error import (
class AuthenticationThrottle(AnonRateThrottle):
rate = "30/minute"
# Rate is configurable per-deployment via the AUTHENTICATION_RATE_LIMIT
# env var (DRF format: "<num>/<period>" where period is second/minute/hour/day).
rate = os.environ.get("AUTHENTICATION_RATE_LIMIT", "10/minute")
scope = "authentication"
def throttle_failure_view(self, request, *args, **kwargs):
@@ -28,6 +33,22 @@ class AuthenticationThrottle(AnonRateThrottle):
return Response(e.get_error_dict(), status=status.HTTP_429_TOO_MANY_REQUESTS)
def authentication_throttle_allows(request):
"""
Apply AuthenticationThrottle to a plain django.views.View request.
DRF's throttle_classes only run inside APIView.initial(); the magic
sign-in / sign-up endpoints extend django.views.View to return
HttpResponseRedirect from a form POST flow, so they need a manual
throttle check. Returns True if the request is allowed through,
False if it should be rejected with a RATE_LIMIT_EXCEEDED error.
"""
throttle = AuthenticationThrottle()
# SimpleRateThrottle.allow_request only reads request.META and
# request.user, both available on a plain Django HttpRequest.
return throttle.allow_request(request, None)
class EmailVerificationThrottle(UserRateThrottle):
"""
Throttle for email verification code generation.
@@ -26,7 +26,10 @@ from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
from plane.authentication.rate_limit import AuthenticationThrottle
from plane.authentication.rate_limit import (
AuthenticationThrottle,
authentication_throttle_allows,
)
from plane.utils.path_validator import get_safe_redirect_url
@@ -65,6 +68,18 @@ class MagicSignInEndpoint(View):
email = request.POST.get("email", "").strip().lower()
next_path = request.POST.get("next_path")
if not authentication_throttle_allows(request):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
error_message="RATE_LIMIT_EXCEEDED",
)
url = get_safe_redirect_url(
base_url=base_host(request=request, is_app=True),
next_path=next_path,
params=exc.get_error_dict(),
)
return HttpResponseRedirect(url)
if code == "" or email == "":
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED"],
@@ -136,6 +151,18 @@ class MagicSignUpEndpoint(View):
email = request.POST.get("email", "").strip().lower()
next_path = request.POST.get("next_path")
if not authentication_throttle_allows(request):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
error_message="RATE_LIMIT_EXCEEDED",
)
url = get_safe_redirect_url(
base_url=base_host(request=request, is_app=True),
next_path=next_path,
params=exc.get_error_dict(),
)
return HttpResponseRedirect(url)
if code == "" or email == "":
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED"],
@@ -25,12 +25,18 @@ from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
from plane.authentication.rate_limit import (
AuthenticationThrottle,
authentication_throttle_allows,
)
from plane.utils.path_validator import get_safe_redirect_url, validate_next_path, get_allowed_hosts
class MagicGenerateSpaceEndpoint(APIView):
permission_classes = [AllowAny]
throttle_classes = [AuthenticationThrottle]
def post(self, request):
# Check if instance is configured
instance = Instance.objects.first()
@@ -60,6 +66,18 @@ class MagicSignInSpaceEndpoint(View):
email = request.POST.get("email", "").strip().lower()
next_path = request.POST.get("next_path")
if not authentication_throttle_allows(request):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
error_message="RATE_LIMIT_EXCEEDED",
)
url = get_safe_redirect_url(
base_url=base_host(request=request, is_space=True),
next_path=next_path,
params=exc.get_error_dict(),
)
return HttpResponseRedirect(url)
if code == "" or email == "":
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED"],
@@ -119,6 +137,18 @@ class MagicSignUpSpaceEndpoint(View):
email = request.POST.get("email", "").strip().lower()
next_path = request.POST.get("next_path")
if not authentication_throttle_allows(request):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
error_message="RATE_LIMIT_EXCEEDED",
)
url = get_safe_redirect_url(
base_url=base_host(request=request, is_space=True),
next_path=next_path,
params=exc.get_error_dict(),
)
return HttpResponseRedirect(url)
if code == "" or email == "":
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED"],
@@ -0,0 +1,143 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
"""Contract tests for the public REST API ``GenericAssetEndpoint``.
Regression coverage for the cross-workspace asset IDOR (the unfixed
external-API sibling of CVE-2026-46558 / GHSA-qw87-v5w3-6vxx). The endpoint
must reject any caller that is not an active member of the workspace named in
the URL slug, regardless of the workspace their Personal Access Token came
from.
"""
from unittest import mock
from uuid import uuid4
import pytest
from rest_framework import status
from plane.db.models import FileAsset, User, Workspace, WorkspaceMember
@pytest.fixture
def victim_user(db):
"""A user that owns a separate workspace the attacker is not part of."""
unique_id = uuid4().hex[:8]
user = User.objects.create(
email=f"victim-{unique_id}@plane.so",
username=f"victim_{unique_id}",
first_name="Victim",
last_name="User",
)
user.set_password("test-password")
user.save()
return user
@pytest.fixture
def victim_workspace(db, victim_user):
"""A workspace whose only active member is ``victim_user``.
The attacker (``create_user``, who authenticates ``api_key_client``) is
deliberately NOT a member here.
"""
workspace = Workspace.objects.create(
name="Victim Workspace",
owner=victim_user,
slug="victim-workspace",
)
WorkspaceMember.objects.create(workspace=workspace, member=victim_user, role=20)
return workspace
@pytest.fixture
def victim_asset(db, victim_workspace, victim_user):
"""An uploaded attachment that lives inside the victim workspace.
``storage_metadata`` is pre-populated so the PATCH handler does not enqueue
the metadata Celery task during the test.
"""
return FileAsset.objects.create(
attributes={"name": "secret.pdf", "type": "application/pdf", "size": 1024},
asset=f"{victim_workspace.id}/secret.pdf",
size=1024,
workspace=victim_workspace,
created_by=victim_user,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
is_uploaded=True,
storage_metadata={"size": 1024},
)
@pytest.mark.contract
class TestGenericAssetCrossWorkspaceIDOR:
"""A PAT holder must not reach assets in a workspace they don't belong to."""
def detail_url(self, slug, asset_id):
return f"/api/v1/workspaces/{slug}/assets/{asset_id}/"
def list_url(self, slug):
return f"/api/v1/workspaces/{slug}/assets/"
@pytest.mark.django_db
def test_get_cross_workspace_asset_returns_403(self, api_key_client, victim_workspace, victim_asset):
"""GET on another workspace's asset must be forbidden, not return a
presigned download URL."""
url = self.detail_url(victim_workspace.slug, victim_asset.id)
with mock.patch("plane.api.views.asset.S3Storage") as mock_storage:
mock_storage.return_value.generate_presigned_url.return_value = "https://signed.example/download"
response = api_key_client.get(url)
assert response.status_code == status.HTTP_403_FORBIDDEN, f"Got {response.status_code}: {response.data!r}"
# The S3 download URL must never be minted for a non-member.
mock_storage.return_value.generate_presigned_url.assert_not_called()
@pytest.mark.django_db
def test_post_cross_workspace_asset_returns_403(self, api_key_client, victim_workspace):
"""POST (upload) into another workspace must be forbidden and must not
plant an asset row in the victim workspace."""
url = self.list_url(victim_workspace.slug)
payload = {"name": "evil.pdf", "type": "application/pdf", "size": 1024}
with mock.patch("plane.api.views.asset.S3Storage") as mock_storage:
mock_storage.return_value.generate_presigned_post.return_value = {"url": "x", "fields": {}}
response = api_key_client.post(url, payload, format="json")
assert response.status_code == status.HTTP_403_FORBIDDEN, f"Got {response.status_code}: {response.data!r}"
assert FileAsset.objects.filter(workspace=victim_workspace).count() == 0
@pytest.mark.django_db
def test_patch_cross_workspace_asset_returns_403(self, api_key_client, victim_workspace, victim_asset):
"""PATCH on another workspace's asset must be forbidden and must leave
the asset untouched."""
url = self.detail_url(victim_workspace.slug, victim_asset.id)
response = api_key_client.patch(url, {"is_uploaded": False}, format="json")
assert response.status_code == status.HTTP_403_FORBIDDEN, f"Got {response.status_code}: {response.data!r}"
victim_asset.refresh_from_db()
assert victim_asset.is_uploaded is True
@pytest.mark.django_db
def test_member_can_patch_own_workspace_asset(self, api_key_client, workspace, create_user):
"""Positive control: an active member of the workspace can still update
their own asset, so the fix does not over-block legitimate callers."""
asset = FileAsset.objects.create(
attributes={"name": "mine.pdf", "type": "application/pdf", "size": 10},
asset=f"{workspace.id}/mine.pdf",
size=10,
workspace=workspace,
created_by=create_user,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
is_uploaded=False,
storage_metadata={"size": 10},
)
url = self.detail_url(workspace.slug, asset.id)
response = api_key_client.patch(url, {"is_uploaded": True}, format="json")
assert response.status_code == status.HTTP_204_NO_CONTENT, f"Got {response.status_code}: {response.data!r}"
asset.refresh_from_db()
assert asset.is_uploaded is True
@@ -5,6 +5,7 @@
import json
import uuid
import pytest
from django.core.cache import cache
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
@@ -12,6 +13,8 @@ from django.test import Client
from django.core.exceptions import ValidationError
from unittest.mock import patch
from plane.authentication.provider.credentials.magic_code import MagicCodeProvider
from plane.authentication.rate_limit import AuthenticationThrottle
from plane.db.models import User
from plane.settings.redis import redis_instance
from plane.license.models import Instance
@@ -428,3 +431,198 @@ class TestMagicSignUp:
# Check if user is authenticated
assert "_auth_user_id" in django_client.session
def _generate_magic_token(api_client, email):
"""Hit /magic-generate/ for `email` and return the token that landed in Redis."""
gen_url = reverse("magic-generate")
response = api_client.post(gen_url, {"email": email}, format="json")
assert response.status_code == status.HTTP_200_OK
ri = redis_instance()
return json.loads(ri.get(f"magic_{email}"))["token"]
@pytest.mark.contract
class TestMagicSignInVerifyAttempts:
"""Per-token wrong-code attempt counter and exhaustion behavior (GHSA-9pvm-fcf6-9234)."""
EMAIL = "verify-attempts@plane.so"
@pytest.fixture
def setup_user(self, db):
user = User.objects.create(email=self.EMAIL)
user.set_password("user@123")
user.save()
return user
@pytest.fixture(autouse=True)
def _clear_state(self):
"""Reset throttle cache and magic-link redis state between tests in this class."""
cache.clear()
ri = redis_instance()
ri.delete(f"magic_{self.EMAIL}")
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
yield
cache.clear()
ri.delete(f"magic_{self.EMAIL}")
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
@pytest.mark.django_db
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
def test_exhausted_after_max_wrong_attempts(
self, mock_magic_link, django_client, api_client, setup_user, setup_instance
):
"""
After MAX_VERIFY_ATTEMPTS wrong codes the next verify must redirect with
EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN and both Redis keys must be gone.
With MAX_VERIFY_ATTEMPTS=5 the 5th wrong attempt itself triggers exhaustion
(4 INVALID + 1 EXHAUSTED), matching the >= check in set_user_data.
"""
_generate_magic_token(api_client, self.EMAIL)
url = reverse("magic-sign-in")
ri = redis_instance()
# First (MAX-1) wrong attempts: each redirects with INVALID_MAGIC_CODE_SIGN_IN.
for i in range(MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 1):
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert response.status_code == 302, f"attempt {i+1} unexpected status"
assert "INVALID_MAGIC_CODE_SIGN_IN" in response.url, f"attempt {i+1} did not return INVALID"
# Token and counter both still live, with counter at MAX-1.
assert ri.exists(f"magic_{self.EMAIL}")
assert int(ri.get(f"magic_{self.EMAIL}:verify_attempts")) == MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 1
# The MAX-th wrong attempt is the exhausting one.
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert response.status_code == 302
assert "EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN" in response.url
# Both the token and the counter must be deleted.
assert not ri.exists(f"magic_{self.EMAIL}")
assert not ri.exists(f"magic_{self.EMAIL}:verify_attempts")
# Follow-up verify now sees the key as missing and reports EXPIRED.
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert response.status_code == 302
assert "EXPIRED_MAGIC_CODE_SIGN_IN" in response.url
@pytest.mark.django_db
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
def test_counter_increments_on_each_wrong_attempt(
self, mock_magic_link, django_client, api_client, setup_user, setup_instance
):
"""The verify_attempts counter increments by exactly one per wrong-code POST."""
_generate_magic_token(api_client, self.EMAIL)
url = reverse("magic-sign-in")
ri = redis_instance()
counter_key = f"magic_{self.EMAIL}:verify_attempts"
# Before any wrong attempt the counter does not exist (Lua INCR creates it).
assert not ri.exists(counter_key)
for expected in range(1, MagicCodeProvider.MAX_VERIFY_ATTEMPTS):
django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert int(ri.get(counter_key)) == expected, f"counter mismatch after {expected} attempts"
@pytest.mark.django_db
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
def test_counter_resets_on_token_regeneration(
self, mock_magic_link, django_client, api_client, setup_user, setup_instance
):
"""
Regenerating the magic-link must reset the verify-attempt counter so the
user isn't pre-locked-out by a previous session's wrong attempts.
"""
_generate_magic_token(api_client, self.EMAIL)
url = reverse("magic-sign-in")
ri = redis_instance()
counter_key = f"magic_{self.EMAIL}:verify_attempts"
for _ in range(MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 2):
django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert int(ri.get(counter_key)) == MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 2
# Regenerate the magic-link — the counter should be cleared.
_generate_magic_token(api_client, self.EMAIL)
assert not ri.exists(counter_key)
# Fresh wrong attempt now produces INVALID (not EXHAUSTED) and counter starts at 1.
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert "INVALID_MAGIC_CODE_SIGN_IN" in response.url
assert int(ri.get(counter_key)) == 1
@pytest.mark.contract
class TestMagicSignUpVerifyAttempts:
"""Sign-up flow gets the same per-token attempt cap (no existing User row)."""
EMAIL = "signup-verify-attempts@plane.so"
@pytest.fixture(autouse=True)
def _clear_state(self):
cache.clear()
ri = redis_instance()
ri.delete(f"magic_{self.EMAIL}")
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
yield
cache.clear()
ri.delete(f"magic_{self.EMAIL}")
ri.delete(f"magic_{self.EMAIL}:verify_attempts")
@pytest.mark.django_db
@patch("plane.bgtasks.magic_link_code_task.magic_link.delay")
def test_signup_exhausted_after_max_wrong_attempts(
self, mock_magic_link, django_client, api_client, setup_instance
):
"""The MAX-th wrong code on the sign-up endpoint returns the SIGN_UP variant of EXHAUSTED."""
_generate_magic_token(api_client, self.EMAIL)
url = reverse("magic-sign-up")
ri = redis_instance()
for _ in range(MagicCodeProvider.MAX_VERIFY_ATTEMPTS - 1):
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert "INVALID_MAGIC_CODE_SIGN_UP" in response.url
response = django_client.post(url, {"email": self.EMAIL, "code": "000000"}, follow=False)
assert "EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP" in response.url
assert not ri.exists(f"magic_{self.EMAIL}")
assert not ri.exists(f"magic_{self.EMAIL}:verify_attempts")
@pytest.mark.contract
class TestAuthenticationThrottle:
"""Per-IP throttle on the redirect-flow magic-link endpoints."""
@pytest.fixture(autouse=True)
def _clear_state(self):
cache.clear()
yield
cache.clear()
@pytest.mark.django_db
def test_magic_sign_in_throttled(self, django_client, setup_instance):
"""Posting past the configured rate from one IP returns RATE_LIMIT_EXCEEDED."""
url = reverse("magic-sign-in")
# Drop the rate so the test doesn't have to fire 10+ requests.
with patch.object(AuthenticationThrottle, "rate", "2/minute"):
for _ in range(2):
response = django_client.post(url, {"email": "throttle@plane.so", "code": "000000"}, follow=False)
assert response.status_code == 302
assert "RATE_LIMIT_EXCEEDED" not in response.url
# The 3rd request from the same IP within the window trips the throttle.
response = django_client.post(url, {"email": "throttle@plane.so", "code": "000000"}, follow=False)
assert response.status_code == 302
assert "RATE_LIMIT_EXCEEDED" in response.url
@pytest.mark.django_db
def test_magic_sign_up_throttled(self, django_client, setup_instance):
"""The sign-up sibling shares the same scope and trips on the same per-IP budget."""
url = reverse("magic-sign-up")
with patch.object(AuthenticationThrottle, "rate", "1/minute"):
response = django_client.post(url, {"email": "throttle-up@plane.so", "code": "000000"}, follow=False)
assert "RATE_LIMIT_EXCEEDED" not in response.url
response = django_client.post(url, {"email": "throttle-up@plane.so", "code": "000000"}, follow=False)
assert "RATE_LIMIT_EXCEEDED" in response.url
@@ -110,7 +110,7 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
if (setToFavorite) {
handleAddToFavorites(res.id);
}
handleNextStep(res.id);
return handleNextStep(res.id);
})
.catch((err) => {
try {
@@ -119,8 +119,9 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
const nameError = errorData.name?.includes("PROJECT_NAME_ALREADY_EXIST");
const identifierError = errorData?.identifier?.includes("PROJECT_IDENTIFIER_ALREADY_EXIST");
const nameSpecialCharError = errorData?.name?.includes("PROJECT_NAME_CANNOT_CONTAIN_SPECIAL_CHARACTERS");
if (nameError || identifierError) {
if (nameError || identifierError || nameSpecialCharError) {
if (nameError) {
setToast({
type: TOAST_TYPE.ERROR,
@@ -136,6 +137,14 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
message: t("project_identifier_already_taken"),
});
}
if (nameSpecialCharError) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("project_name_cannot_contain_special_characters"),
});
}
} else {
setToast({
type: TOAST_TYPE.ERROR,
+10 -1
View File
@@ -105,8 +105,9 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
const nameError = errorData.name?.includes("PROJECT_NAME_ALREADY_EXIST");
const identifierError = errorData?.identifier?.includes("PROJECT_IDENTIFIER_ALREADY_EXIST");
const nameSpecialCharError = errorData?.name?.includes("PROJECT_NAME_CANNOT_CONTAIN_SPECIAL_CHARACTERS");
if (nameError || identifierError) {
if (nameError || identifierError || nameSpecialCharError) {
if (nameError) {
setToast({
type: TOAST_TYPE.ERROR,
@@ -122,6 +123,14 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
message: t("project_identifier_already_taken"),
});
}
if (nameSpecialCharError) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("project_name_cannot_contain_special_characters"),
});
}
} else {
setToast({
type: TOAST_TYPE.ERROR,
+5
View File
@@ -49,6 +49,11 @@ MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT=60/minute
# Per-IP throttle for anonymous authentication endpoints (magic-link
# generate / sign-in / sign-up, email sign-in). DRF format: "<n>/<period>"
# where period is second/minute/hour/day.
AUTHENTICATION_RATE_LIMIT=10/minute
# Live Server Secret Key
LIVE_SERVER_SECRET_KEY=htbqvBJAgpm9bzvf3r4urJer0ENReatceh
@@ -117,8 +117,6 @@ services:
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
- plane-mq
worker:
@@ -134,8 +132,6 @@ services:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
beat-worker:
@@ -151,8 +147,6 @@ services:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
migrator:
@@ -166,31 +160,6 @@ services:
- logs_migrator:/code/plane/logs
environment:
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
depends_on:
- plane-db
- plane-redis
# Comment this if you already have a database running
plane-db:
image: postgres:15.7-alpine
command: postgres -c 'max_connections=1000'
deploy:
replicas: 1
restart_policy:
condition: any
environment:
<<: *db-env
volumes:
- pgdata:/var/lib/postgresql/data
plane-redis:
image: valkey/valkey:7.2.11-alpine
deploy:
replicas: 1
restart_policy:
condition: any
volumes:
- redisdata:/data
plane-mq:
image: rabbitmq:3.13.6-management-alpine
@@ -230,10 +199,6 @@ services:
published: ${LISTEN_HTTP_PORT:-80}
protocol: tcp
mode: host
- target: 443
published: ${LISTEN_HTTPS_PORT:-443}
protocol: tcp
mode: host
volumes:
- proxy_config:/config
- proxy_data:/data
@@ -245,8 +210,6 @@ services:
- live
volumes:
pgdata:
redisdata:
uploads:
logs_api:
logs_worker:
+5
View File
@@ -77,6 +77,11 @@ MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT=60/minute
# Per-IP throttle for anonymous authentication endpoints (magic-link
# generate / sign-in / sign-up, email sign-in). DRF format: "<n>/<period>"
# where period is second/minute/hour/day.
AUTHENTICATION_RATE_LIMIT=10/minute
# Live server environment variables
# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
LIVE_SERVER_SECRET_KEY=
-1
View File
@@ -1 +0,0 @@
src/locales
+19 -9
View File
@@ -189,6 +189,11 @@ function CustomMenu(props: ICustomMenuDropdownProps) {
}
}, [isOpen, closeDropdown, useCaptureForOutsideClick]);
const menuContextValue = React.useMemo(
() => ({ closeAllSubmenus, registerSubmenu }),
[closeAllSubmenus, registerSubmenu]
);
let menuItems = (
<Menu.Items
data-prevent-outside-click={!!portalElement}
@@ -200,7 +205,7 @@ function CustomMenu(props: ICustomMenuDropdownProps) {
>
<div
className={cn(
"my-1 min-w-[12rem] overflow-y-scroll rounded-md border-[0.5px] border-subtle-1 bg-surface-1 px-2 py-2.5 text-11 whitespace-nowrap focus:outline-none",
"shadow-md my-1 min-w-[12rem] overflow-y-scroll rounded-md border border-strong-1 bg-surface-1 px-2 py-2.5 text-11 whitespace-nowrap ring-1 ring-strong-1/15 outline-none focus:outline-none",
{
"max-h-60": maxHeight === "lg",
"max-h-48": maxHeight === "md",
@@ -213,7 +218,7 @@ function CustomMenu(props: ICustomMenuDropdownProps) {
style={styles.popper}
{...attributes.popper}
>
<MenuContext.Provider value={{ closeAllSubmenus, registerSubmenu }}>{children}</MenuContext.Provider>
<MenuContext.Provider value={menuContextValue}>{children}</MenuContext.Provider>
</div>
</Menu.Items>
);
@@ -228,7 +233,8 @@ function CustomMenu(props: ICustomMenuDropdownProps) {
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("relative w-min text-left", className)}
onKeyDownCapture={handleKeyDown}
onKeyDown={handleKeyDown}
role="presentation"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
@@ -377,6 +383,8 @@ function SubMenu(props: ICustomSubMenuProps) {
toggleSubmenu();
};
const subMenuContextValue = React.useMemo(() => ({ closeSubmenu }), [closeSubmenu]);
// Close submenu when clicking on other menu items
React.useEffect(() => {
const handleMenuItemClick = (e: Event) => {
@@ -398,9 +406,10 @@ function SubMenu(props: ICustomSubMenuProps) {
<span ref={setReferenceElement} className="w-full">
<Menu.Item as="div" disabled={disabled}>
{({ active }) => (
<div
<button
type="button"
className={cn(
"flex w-full cursor-pointer items-center justify-between rounded-sm px-1 py-1.5 text-left text-secondary select-none",
"font-inherit flex w-full cursor-pointer items-center justify-between rounded-sm border-0 bg-transparent px-1 py-1.5 text-left text-secondary outline-none select-none",
{
"bg-layer-transparent-hover": active && !disabled,
"text-placeholder": disabled,
@@ -408,10 +417,11 @@ function SubMenu(props: ICustomSubMenuProps) {
}
)}
onClick={handleClick}
disabled={disabled}
>
<span className="flex-1">{trigger}</span>
<ChevronRightIcon className="h-3.5 w-3.5 flex-shrink-0" />
</div>
</button>
)}
</Menu.Item>
</span>
@@ -423,7 +433,7 @@ function SubMenu(props: ICustomSubMenuProps) {
style={styles.popper}
{...attributes.popper}
className={cn(
"fixed z-30 min-w-[12rem] overflow-hidden rounded-md border-[0.5px] border-subtle-1 bg-surface-1 p-1 text-11",
"shadow-md fixed z-30 min-w-[12rem] overflow-hidden rounded-md border border-strong-1 bg-surface-1 p-1 text-11 ring-1 ring-strong-1/15",
contentClassName
)}
data-prevent-outside-click="true"
@@ -444,7 +454,7 @@ function SubMenu(props: ICustomSubMenuProps) {
}
}}
>
<SubMenuContext.Provider value={{ closeSubmenu }}>{children}</SubMenuContext.Provider>
<SubMenuContext.Provider value={subMenuContextValue}>{children}</SubMenuContext.Provider>
</div>
</Portal>
)}
@@ -516,7 +526,7 @@ function SubMenuContent(props: ICustomSubMenuContentProps) {
return (
<div
className={cn(
"z-[15] min-w-[12rem] overflow-hidden rounded-md border border-subtle-1 bg-surface-1 p-1 text-11",
"shadow-md z-[15] min-w-[12rem] overflow-hidden rounded-md border border-strong-1 bg-surface-1 p-1 text-11 ring-1 ring-strong-1/15",
className
)}
>
+165 -195
View File
@@ -91,14 +91,14 @@ catalogs:
specifier: ^2.9.2
version: 2.9.2
'@react-router/dev':
specifier: 7.13.1
version: 7.13.1
specifier: 7.15.0
version: 7.15.0
'@react-router/node':
specifier: 7.13.1
version: 7.13.1
specifier: 7.15.0
version: 7.15.0
'@react-router/serve':
specifier: 7.13.1
version: 7.13.1
specifier: 7.15.0
version: 7.15.0
'@storybook/addon-designs':
specifier: 10.0.2
version: 10.0.2
@@ -265,8 +265,8 @@ catalogs:
specifier: ^8.18.1
version: 8.18.1
'@vitest/coverage-v8':
specifier: ^4.0.8
version: 4.0.17
specifier: ^4.1.8
version: 4.1.8
ast-types:
specifier: 0.14.2
version: 0.14.2
@@ -454,8 +454,8 @@ catalogs:
specifier: ^2.3.0
version: 2.3.0
react-router:
specifier: 7.12.0
version: 7.12.0
specifier: 7.15.0
version: 7.15.0
recharts:
specifier: ^2.15.1
version: 2.15.4
@@ -523,8 +523,8 @@ catalogs:
specifier: ^5.1.4
version: 5.1.4
vitest:
specifier: ^4.0.8
version: 4.0.15
specifier: ^4.1.8
version: 4.1.8
winston:
specifier: ^3.17.0
version: 3.17.0
@@ -649,7 +649,7 @@ importers:
version: link:../../packages/utils
'@react-router/node':
specifier: 'catalog:'
version: 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@tanstack/react-virtual':
specifier: 'catalog:'
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -688,7 +688,7 @@ importers:
version: 7.51.5(react@18.3.1)
react-router:
specifier: 'catalog:'
version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
serve:
specifier: 'catalog:'
version: 14.2.5
@@ -707,7 +707,7 @@ importers:
version: link:../../packages/typescript-config
'@react-router/dev':
specifier: 'catalog:'
version: 7.13.1(@react-router/serve@7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)
version: 7.15.0(@react-router/serve@7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)
'@tailwindcss/postcss':
specifier: 'catalog:'
version: 4.1.17
@@ -870,7 +870,7 @@ importers:
version: 8.18.1
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.0.17(vitest@4.0.15(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))
version: 4.1.8(vitest@4.1.8)
pdf-parse:
specifier: 'catalog:'
version: 2.4.5
@@ -882,7 +882,7 @@ importers:
version: 5.8.3
vitest:
specifier: 'catalog:'
version: 4.0.15(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
version: 4.1.8(@types/node@22.12.0)(@vitest/coverage-v8@4.1.8)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))
apps/space:
dependencies:
@@ -930,10 +930,10 @@ importers:
version: 2.11.8
'@react-router/node':
specifier: 'catalog:'
version: 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/serve':
specifier: 'catalog:'
version: 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
axios:
specifier: 1.16.0
version: 1.16.0
@@ -981,7 +981,7 @@ importers:
version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router:
specifier: 'catalog:'
version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
swr:
specifier: 'catalog:'
version: 2.2.4(react@18.3.1)
@@ -997,7 +997,7 @@ importers:
version: link:../../packages/typescript-config
'@react-router/dev':
specifier: 'catalog:'
version: 7.13.1(@react-router/serve@7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)
version: 7.15.0(@react-router/serve@7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)
'@tailwindcss/postcss':
specifier: 'catalog:'
version: 4.1.17
@@ -1093,7 +1093,7 @@ importers:
version: 4.3.0(react@18.3.1)
'@react-router/node':
specifier: 'catalog:'
version: 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
version: 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@tanstack/react-table':
specifier: 'catalog:'
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1174,7 +1174,7 @@ importers:
version: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router:
specifier: 'catalog:'
version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts:
specifier: 'catalog:'
version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1202,7 +1202,7 @@ importers:
version: link:../../packages/typescript-config
'@react-router/dev':
specifier: 'catalog:'
version: 7.13.1(@react-router/serve@7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)
version: 7.15.0(@react-router/serve@7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)
'@tailwindcss/postcss':
specifier: 'catalog:'
version: 4.1.17
@@ -1253,7 +1253,7 @@ importers:
version: 17.3.0
vitest:
specifier: 'catalog:'
version: 4.0.15(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
version: 4.1.8(@types/node@22.12.0)(@vitest/coverage-v8@4.1.8)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))
packages/constants:
dependencies:
@@ -2019,10 +2019,6 @@ packages:
'@atlaskit/pragmatic-drag-and-drop@1.7.4':
resolution: {integrity: sha512-lZHnO9BJdHPKnwB0uvVUCyDnIhL+WAHzXQ2EXX0qacogOsnvIUiCgY0BLKhBqTCWln3/f/Ox5jU54MKO6ayh9A==}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
@@ -2205,10 +2201,6 @@ packages:
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
@@ -3516,14 +3508,14 @@ packages:
'@react-pdf/types@2.9.2':
resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==}
'@react-router/dev@7.13.1':
resolution: {integrity: sha512-H+kEvbbOaWGaitOyL6CgqPsHqRUh66HuVRvIEaZEqdoAY/1xChdhmmq6ZumMHzcFHgHlfOcoXgNHlz6ZO4NWcg==}
'@react-router/dev@7.15.0':
resolution: {integrity: sha512-ZwUQu4KNZrViFqdeFWqh00Bk/QbLNvoWRDfjsqOp3oyuG3jSRLYnqRD3VAMK/FYMpL+s37ByT7XqqLXaF7Nw1g==}
engines: {node: '>=20.0.0'}
hasBin: true
peerDependencies:
'@react-router/serve': ^7.13.1
'@vitejs/plugin-rsc': ~0.5.7
react-router: ^7.13.1
'@react-router/serve': ^7.15.0
'@vitejs/plugin-rsc': ~0.5.21
react-router: ^7.15.0
react-server-dom-webpack: ^19.2.3
typescript: 5.8.3
vite: 7.3.2
@@ -3540,33 +3532,33 @@ packages:
wrangler:
optional: true
'@react-router/express@7.13.1':
resolution: {integrity: sha512-ujHom4LiEWsbnohNArwNT86QP3WRB5p+rY8AAll6s4gdrzgOXIy3FHDc3up5Lz8juUrZKh0d+B+PZa/IdDSK3A==}
'@react-router/express@7.15.0':
resolution: {integrity: sha512-WldQrI5FxbsTLztOqx0+c7248m8IDUchCUUX177I+qz9OMuLR9ueIqz53zBKCVQ+Zf7k6FyatwpoLmr/Wovalg==}
engines: {node: '>=20.0.0'}
peerDependencies:
express: 4.22.0
react-router: 7.13.1
react-router: 7.15.0
typescript: 5.8.3
peerDependenciesMeta:
typescript:
optional: true
'@react-router/node@7.13.1':
resolution: {integrity: sha512-IWPPf+Q3nJ6q4bwyTf5leeGUfg8GAxSN1RKj5wp9SK915zKK+1u4TCOfOmr8hmC6IW1fcjKV0WChkM0HkReIiw==}
'@react-router/node@7.15.0':
resolution: {integrity: sha512-SgvWaWF1n3u+bpXXZUW9BSd2p/NwkIYLz4SSeDYqoX5RkYX5rcI4cHHuNJXszPu+Dm9QIri4J9g/4EV3KfgiXQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
react-router: 7.13.1
react-router: 7.15.0
typescript: 5.8.3
peerDependenciesMeta:
typescript:
optional: true
'@react-router/serve@7.13.1':
resolution: {integrity: sha512-vh5lr41rioXLz/zNLTYo0zq4yh97AkgEkJK7bhPeXnNbLNtI36WCZ2AeBtSJ4sdx4gx5LZvcjP8zoWFfSbNupA==}
'@react-router/serve@7.15.0':
resolution: {integrity: sha512-0XtYmwc11vWdYn2zeEXx9E3u0I6TH3bm4uDaMdsyI09S6hl6uc98vBkTSXg7Znm3qR82R/jjtn3LvV2QEZ193w==}
engines: {node: '>=20.0.0'}
hasBin: true
peerDependencies:
react-router: 7.13.1
react-router: 7.15.0
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -3814,6 +3806,9 @@ packages:
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@storybook/addon-actions@8.6.14':
resolution: {integrity: sha512-mDQxylxGGCQSK7tJPkD144J8jWh9IU9ziJMHfB84PKpI/V5ZgqMDnpr2bssTrUaGDqU5e1/z8KcRF+Melhs9pQ==}
peerDependencies:
@@ -4771,11 +4766,11 @@ packages:
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@vitest/coverage-v8@4.0.17':
resolution: {integrity: sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==}
'@vitest/coverage-v8@4.1.8':
resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==}
peerDependencies:
'@vitest/browser': 4.0.17
vitest: 4.0.17
'@vitest/browser': 4.1.8
vitest: 4.1.8
peerDependenciesMeta:
'@vitest/browser':
optional: true
@@ -4786,8 +4781,8 @@ packages:
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/expect@4.0.15':
resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==}
'@vitest/expect@4.1.8':
resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==}
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
@@ -4800,8 +4795,8 @@ packages:
vite:
optional: true
'@vitest/mocker@4.0.15':
resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==}
'@vitest/mocker@4.1.8':
resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==}
peerDependencies:
msw: ^2.4.9
vite: 7.3.2
@@ -4820,17 +4815,14 @@ packages:
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/pretty-format@4.0.15':
resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==}
'@vitest/pretty-format@4.1.8':
resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==}
'@vitest/pretty-format@4.0.17':
resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==}
'@vitest/runner@4.1.8':
resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==}
'@vitest/runner@4.0.15':
resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==}
'@vitest/snapshot@4.0.15':
resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==}
'@vitest/snapshot@4.1.8':
resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==}
'@vitest/spy@2.0.5':
resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==}
@@ -4838,8 +4830,8 @@ packages:
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/spy@4.0.15':
resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==}
'@vitest/spy@4.1.8':
resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==}
'@vitest/utils@2.0.5':
resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==}
@@ -4850,11 +4842,8 @@ packages:
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@vitest/utils@4.0.15':
resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==}
'@vitest/utils@4.0.17':
resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==}
'@vitest/utils@4.1.8':
resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==}
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@@ -5035,8 +5024,8 @@ packages:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
ast-v8-to-istanbul@0.3.10:
resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==}
ast-v8-to-istanbul@1.0.3:
resolution: {integrity: sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==}
async-lock@1.4.1:
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
@@ -5204,8 +5193,8 @@ packages:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
chai@6.2.1:
resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
chalk-template@0.4.0:
@@ -6540,12 +6529,12 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
jscodeshift@17.3.0:
resolution: {integrity: sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==}
engines: {node: '>=16'}
@@ -6800,8 +6789,8 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.5.1:
resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==}
magicast@0.5.3:
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
make-dir@2.1.0:
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
@@ -7854,8 +7843,8 @@ packages:
'@types/react':
optional: true
react-router@7.12.0:
resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==}
react-router@7.15.0:
resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -8232,8 +8221,8 @@ packages:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
storybook@9.1.19:
resolution: {integrity: sha512-P7K/b+Pn1sXJzwYCF6hH5Zjdrg4ZlA5Bz9rdOJEdvm6ev27XESDGI+Ql+dfUfUcGOym3Aud4MssJIDEF2ocsyQ==}
@@ -8413,8 +8402,8 @@ packages:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyrainbow@3.0.3:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
tinyrainbow@3.1.0:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
tinyspy@3.0.2:
@@ -8790,20 +8779,23 @@ packages:
yaml:
optional: true
vitest@4.0.15:
resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==}
vitest@4.1.8:
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.0.15
'@vitest/browser-preview': 4.0.15
'@vitest/browser-webdriverio': 4.0.15
'@vitest/ui': 4.0.15
'@vitest/browser-playwright': 4.1.8
'@vitest/browser-preview': 4.1.8
'@vitest/browser-webdriverio': 4.1.8
'@vitest/coverage-istanbul': 4.1.8
'@vitest/coverage-v8': 4.1.8
'@vitest/ui': 4.1.8
happy-dom: '*'
jsdom: '*'
vite: 7.3.2
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
@@ -8817,6 +8809,10 @@ packages:
optional: true
'@vitest/browser-webdriverio':
optional: true
'@vitest/coverage-istanbul':
optional: true
'@vitest/coverage-v8':
optional: true
'@vitest/ui':
optional: true
happy-dom:
@@ -9029,12 +9025,6 @@ snapshots:
bind-event-listener: 3.0.0
raf-schd: 4.0.3
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -9045,15 +9035,15 @@ snapshots:
'@babel/core@7.28.4':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/code-frame': 7.29.7
'@babel/generator': 7.28.5
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4)
'@babel/helpers': 7.26.10
'@babel/parser': 7.28.5
'@babel/template': 7.27.2
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/traverse': 7.28.4
'@babel/types': 7.28.5
'@babel/types': 7.29.7
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
debug: 4.4.3
@@ -9065,8 +9055,8 @@ snapshots:
'@babel/generator@7.28.5':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
@@ -9266,12 +9256,6 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.29.7
'@babel/types': 7.28.5
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
@@ -9280,12 +9264,12 @@ snapshots:
'@babel/traverse@7.28.4':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/code-frame': 7.29.7
'@babel/generator': 7.28.5
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.28.5
'@babel/template': 7.27.2
'@babel/types': 7.28.5
'@babel/parser': 7.29.7
'@babel/template': 7.29.7
'@babel/types': 7.29.7
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -10386,16 +10370,16 @@ snapshots:
'@react-pdf/primitives': 4.1.1
'@react-pdf/stylesheet': 6.1.2
'@react-router/dev@7.13.1(@react-router/serve@7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)':
'@react-router/dev@7.15.0(@react-router/serve@7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3))(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(terser@5.43.1)(tsx@4.20.6)(typescript@5.8.3)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))(yaml@2.8.3)':
dependencies:
'@babel/core': 7.28.4
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
'@babel/parser': 7.29.7
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4)
'@babel/preset-typescript': 7.27.1(@babel/core@7.28.4)
'@babel/traverse': 7.28.4
'@babel/types': 7.28.5
'@react-router/node': 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@babel/types': 7.29.7
'@react-router/node': 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@remix-run/node-fetch-server': 0.13.0
arg: 5.0.2
babel-dead-code-elimination: 1.0.10
@@ -10412,14 +10396,14 @@ snapshots:
pkg-types: 2.3.0
prettier: 3.7.4
react-refresh: 0.14.2
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
semver: 7.7.4
tinyglobby: 0.2.15
valibot: 1.2.0(typescript@5.8.3)
vite: 7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
vite-node: 3.2.4(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
optionalDependencies:
'@react-router/serve': 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/serve': 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
typescript: 5.8.3
transitivePeerDependencies:
- '@types/node'
@@ -10436,31 +10420,31 @@ snapshots:
- tsx
- yaml
'@react-router/express@7.13.1(express@4.22.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
'@react-router/express@7.15.0(express@4.22.0)(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
dependencies:
'@react-router/node': 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
express: 4.22.0
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
typescript: 5.8.3
'@react-router/node@7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
'@react-router/node@7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
dependencies:
'@mjackson/node-fetch-server': 0.2.0
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
typescript: 5.8.3
'@react-router/serve@7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
'@react-router/serve@7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)':
dependencies:
'@mjackson/node-fetch-server': 0.2.0
'@react-router/express': 7.13.1(express@4.22.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.13.1(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/express': 7.15.0(express@4.22.0)(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
'@react-router/node': 7.15.0(react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.8.3)
compression: 1.8.1
express: 4.22.0
get-port: 5.1.1
morgan: 1.10.1
react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router: 7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
source-map-support: 0.5.21
transitivePeerDependencies:
- supports-color
@@ -10601,6 +10585,8 @@ snapshots:
'@standard-schema/spec@1.0.0': {}
'@standard-schema/spec@1.1.0': {}
'@storybook/addon-actions@8.6.14(storybook@9.1.19(@testing-library/dom@10.4.0)(prettier@3.7.4)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)))':
dependencies:
'@storybook/global': 5.0.0
@@ -11678,19 +11664,19 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@vitest/coverage-v8@4.0.17(vitest@4.0.15(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))':
'@vitest/coverage-v8@4.1.8(vitest@4.1.8)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.17
ast-v8-to-istanbul: 0.3.10
'@vitest/utils': 4.1.8
ast-v8-to-istanbul: 1.0.3
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.1
magicast: 0.5.3
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.15(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.8(@types/node@22.12.0)(@vitest/coverage-v8@4.1.8)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))
'@vitest/expect@2.0.5':
dependencies:
@@ -11707,14 +11693,14 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/expect@4.0.15':
'@vitest/expect@4.1.8':
dependencies:
'@standard-schema/spec': 1.0.0
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.2
'@vitest/spy': 4.0.15
'@vitest/utils': 4.0.15
chai: 6.2.1
tinyrainbow: 3.0.3
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@3.2.4(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))':
dependencies:
@@ -11724,9 +11710,9 @@ snapshots:
optionalDependencies:
vite: 7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
'@vitest/mocker@4.0.15(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))':
'@vitest/mocker@4.1.8(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 4.0.15
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
@@ -11744,22 +11730,19 @@ snapshots:
dependencies:
tinyrainbow: 2.0.0
'@vitest/pretty-format@4.0.15':
'@vitest/pretty-format@4.1.8':
dependencies:
tinyrainbow: 3.0.3
tinyrainbow: 3.1.0
'@vitest/pretty-format@4.0.17':
'@vitest/runner@4.1.8':
dependencies:
tinyrainbow: 3.0.3
'@vitest/runner@4.0.15':
dependencies:
'@vitest/utils': 4.0.15
'@vitest/utils': 4.1.8
pathe: 2.0.3
'@vitest/snapshot@4.0.15':
'@vitest/snapshot@4.1.8':
dependencies:
'@vitest/pretty-format': 4.0.15
'@vitest/pretty-format': 4.1.8
'@vitest/utils': 4.1.8
magic-string: 0.30.21
pathe: 2.0.3
@@ -11771,7 +11754,7 @@ snapshots:
dependencies:
tinyspy: 4.0.3
'@vitest/spy@4.0.15': {}
'@vitest/spy@4.1.8': {}
'@vitest/utils@2.0.5':
dependencies:
@@ -11792,15 +11775,11 @@ snapshots:
loupe: 3.2.1
tinyrainbow: 2.0.0
'@vitest/utils@4.0.15':
'@vitest/utils@4.1.8':
dependencies:
'@vitest/pretty-format': 4.0.15
tinyrainbow: 3.0.3
'@vitest/utils@4.0.17':
dependencies:
'@vitest/pretty-format': 4.0.17
tinyrainbow: 3.0.3
'@vitest/pretty-format': 4.1.8
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
'@webassemblyjs/ast@1.14.1':
dependencies:
@@ -11990,11 +11969,11 @@ snapshots:
dependencies:
tslib: 2.8.1
ast-v8-to-istanbul@0.3.10:
ast-v8-to-istanbul@1.0.3:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 9.0.1
js-tokens: 10.0.0
async-lock@1.4.1: {}
@@ -12029,9 +12008,9 @@ snapshots:
babel-dead-code-elimination@1.0.10:
dependencies:
'@babel/core': 7.28.4
'@babel/parser': 7.28.5
'@babel/parser': 7.29.7
'@babel/traverse': 7.28.4
'@babel/types': 7.28.5
'@babel/types': 7.29.7
transitivePeerDependencies:
- supports-color
@@ -12182,7 +12161,7 @@ snapshots:
loupe: 3.2.1
pathval: 2.0.1
chai@6.2.1: {}
chai@6.2.2: {}
chalk-template@0.4.0:
dependencies:
@@ -13556,9 +13535,9 @@ snapshots:
jiti@2.6.1: {}
js-tokens@4.0.0: {}
js-tokens@10.0.0: {}
js-tokens@9.0.1: {}
js-tokens@4.0.0: {}
jscodeshift@17.3.0:
dependencies:
@@ -13804,7 +13783,7 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magicast@0.5.1:
magicast@0.5.3:
dependencies:
'@babel/parser': 7.29.7
'@babel/types': 7.29.7
@@ -15167,7 +15146,7 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.11
react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-router@7.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
cookie: 1.0.2
react: 18.3.1
@@ -15703,7 +15682,7 @@ snapshots:
statuses@2.0.1: {}
std-env@3.10.0: {}
std-env@4.1.0: {}
storybook@9.1.19(@testing-library/dom@10.4.0)(prettier@3.7.4)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)):
dependencies:
@@ -15874,7 +15853,7 @@ snapshots:
tinyrainbow@2.0.0: {}
tinyrainbow@3.0.3: {}
tinyrainbow@3.1.0: {}
tinyspy@3.0.2: {}
@@ -16279,42 +16258,33 @@ snapshots:
tsx: 4.20.6
yaml: 2.8.3
vitest@4.0.15(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3):
vitest@4.1.8(@types/node@22.12.0)(@vitest/coverage-v8@4.1.8)(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.0.15
'@vitest/mocker': 4.0.15(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))
'@vitest/pretty-format': 4.0.15
'@vitest/runner': 4.0.15
'@vitest/snapshot': 4.0.15
'@vitest/spy': 4.0.15
'@vitest/utils': 4.0.15
es-module-lexer: 1.7.0
'@vitest/expect': 4.1.8
'@vitest/mocker': 4.1.8(vite@7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3))
'@vitest/pretty-format': 4.1.8
'@vitest/runner': 4.1.8
'@vitest/snapshot': 4.1.8
'@vitest/spy': 4.1.8
'@vitest/utils': 4.1.8
es-module-lexer: 2.0.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 2.3.2
std-env: 3.10.0
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.0.4
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
tinyrainbow: 3.1.0
vite: 7.3.2(@types/node@22.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.12.0
'@vitest/coverage-v8': 4.1.8(vitest@4.1.8)
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
void-elements@3.1.0: {}
+6 -6
View File
@@ -33,9 +33,9 @@ catalog:
"@radix-ui/react-scroll-area": "^1.2.3"
"@react-pdf/renderer": "^4.3.0"
"@react-pdf/types": "^2.9.2"
"@react-router/dev": "7.13.1"
"@react-router/node": "7.13.1"
"@react-router/serve": "7.13.1"
"@react-router/dev": "7.15.0"
"@react-router/node": "7.15.0"
"@react-router/serve": "7.15.0"
"@storybook/addon-designs": "10.0.2"
"@storybook/addon-docs": "9.1.10"
"@storybook/addon-essentials": "^8.1.1"
@@ -92,7 +92,7 @@ catalog:
"@types/react-dom": "18.3.1"
"@types/sanitize-html": "2.16.0"
"@types/ws": "^8.18.1"
"@vitest/coverage-v8": "^4.0.8"
"@vitest/coverage-v8": "^4.1.8"
"ast-types": "0.14.2"
"autoprefixer": "^10.4.19"
"axios": "1.16.0"
@@ -159,7 +159,7 @@ catalog:
"react-masonry-component": "^6.3.0"
"react-pdf-html": "^2.1.2"
"react-popper": "^2.3.0"
"react-router": "7.12.0"
"react-router": "7.15.0"
"recharts": "^2.15.1"
"reflect-metadata": "^0.2.2"
"rehype-parse": "^9.0.1"
@@ -185,7 +185,7 @@ catalog:
"uuid": "14.0.0"
"vite": "7.3.2"
"vite-tsconfig-paths": "^5.1.4"
"vitest": "^4.0.8"
"vitest": "^4.1.8"
"winston": "^3.17.0"
"ws": "8.20.1"
"y-indexeddb": "^9.0.12"