diff --git a/.github/workflows/wasm-build-validation.yml b/.github/workflows/wasm-build-validation.yml index 5f21472e5..e821e6234 100644 --- a/.github/workflows/wasm-build-validation.yml +++ b/.github/workflows/wasm-build-validation.yml @@ -66,7 +66,7 @@ jobs: echo "Size: ${SIZE} bytes (${SIZE_MB} MB)" - if [ ${SIZE} -gt 62914560 ]; then - echo "Wasm binary size (${SIZE_MB}MB) exceeds 60MB limit!" + if [ ${SIZE} -gt 73400320 ]; then + echo "Wasm binary size (${SIZE_MB}MB) exceeds 70MB limit!" exit 1 fi diff --git a/client/embed/embed.go b/client/embed/embed.go index 208e71817..5905c1cf6 100644 --- a/client/embed/embed.go +++ b/client/embed/embed.go @@ -12,11 +12,10 @@ import ( "sync" "github.com/sirupsen/logrus" - wgdevice "golang.zx2c4.com/wireguard/device" - wgnetstack "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/netstack" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/auth" "github.com/netbirdio/netbird/client/internal/peer" @@ -256,10 +255,10 @@ func New(opts Options) (*Client, error) { } if opts.Performance.PreallocatedBuffersPerPool != nil { - wgdevice.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool) + wgbackend.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool) } if opts.Performance.MaxBatchSize != nil { - wgdevice.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize) + wgbackend.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize) } return &Client{ @@ -651,7 +650,7 @@ func (c *Client) getEngine() (*internal.Engine, error) { return engine, nil } -func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) { +func (c *Client) getNet() (*wgbackend.NetstackNet, netip.Addr, error) { engine, err := c.getEngine() if err != nil { return nil, netip.Addr{}, err diff --git a/client/firewall/iface.go b/client/firewall/iface.go index 491f03269..dc172bba7 100644 --- a/client/firewall/iface.go +++ b/client/firewall/iface.go @@ -1,10 +1,9 @@ package firewall import ( - wgdevice "golang.zx2c4.com/wireguard/device" - "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // EnvForceUserspaceFirewall forces the use of the userspace packet filter even when @@ -20,5 +19,5 @@ type IFaceMapper interface { IsUserspaceBind() bool SetFilter(device.PacketFilter) error GetDevice() *device.FilteredDevice - GetWGDevice() *wgdevice.Device + GetWGDevice() *wgbackend.Device } diff --git a/client/firewall/uspfilter/filter.go b/client/firewall/uspfilter/filter.go index 0c73400f1..56441a639 100644 --- a/client/firewall/uspfilter/filter.go +++ b/client/firewall/uspfilter/filter.go @@ -19,7 +19,6 @@ import ( "github.com/google/uuid" "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" - wgdevice "golang.zx2c4.com/wireguard/device" nberrors "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/firewall/firewalld" @@ -31,6 +30,7 @@ import ( "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" nbid "github.com/netbirdio/netbird/client/internal/acl/id" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/statemanager" @@ -121,7 +121,7 @@ type Iface interface { Name() string Address() wgaddr.Address SetFilter(device.PacketFilter) error - GetWGDevice() *wgdevice.Device + GetWGDevice() *wgbackend.Device } // InterfaceAllower opens the NetBird interface in the host firewall so it diff --git a/client/firewall/uspfilter/filter_filter_test.go b/client/firewall/uspfilter/filter_filter_test.go index 57b59c9bc..d6e9f4f72 100644 --- a/client/firewall/uspfilter/filter_filter_test.go +++ b/client/firewall/uspfilter/filter_filter_test.go @@ -5,17 +5,17 @@ import ( "net/netip" "testing" - "go.uber.org/mock/gomock" "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/stretchr/testify/require" - wgdevice "golang.zx2c4.com/wireguard/device" + "go.uber.org/mock/gomock" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/mocks" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/shared/management/domain" ) @@ -791,10 +791,10 @@ func setupRoutedManager(tb testing.TB, network string) *Manager { } }, GetDeviceFunc: func() *device.FilteredDevice { - return &device.FilteredDevice{Device: dev} + return &device.FilteredDevice{TUN: dev} }, - GetWGDeviceFunc: func() *wgdevice.Device { - return &wgdevice.Device{} + GetWGDeviceFunc: func() *wgbackend.Device { + return &wgbackend.Device{} }, } diff --git a/client/firewall/uspfilter/filter_routeacl_test.go b/client/firewall/uspfilter/filter_routeacl_test.go index 2a0980e83..8c27aa4db 100644 --- a/client/firewall/uspfilter/filter_routeacl_test.go +++ b/client/firewall/uspfilter/filter_routeacl_test.go @@ -4,17 +4,17 @@ import ( "net/netip" "testing" - "go.uber.org/mock/gomock" "github.com/google/gopacket/layers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - wgdevice "golang.zx2c4.com/wireguard/device" + "go.uber.org/mock/gomock" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/mocks" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // TestAddRouteFilteringReturnsExistingRule verifies that adding the same route @@ -180,10 +180,10 @@ func TestBlockInvalidRoutedDualStack(t *testing.T) { } }, GetDeviceFunc: func() *device.FilteredDevice { - return &device.FilteredDevice{Device: dev} + return &device.FilteredDevice{TUN: dev} }, - GetWGDeviceFunc: func() *wgdevice.Device { - return &wgdevice.Device{} + GetWGDeviceFunc: func() *wgbackend.Device { + return &wgbackend.Device{} }, } @@ -228,10 +228,10 @@ func TestBlockInvalidRoutedIdempotent(t *testing.T) { } }, GetDeviceFunc: func() *device.FilteredDevice { - return &device.FilteredDevice{Device: dev} + return &device.FilteredDevice{TUN: dev} }, - GetWGDeviceFunc: func() *wgdevice.Device { - return &wgdevice.Device{} + GetWGDeviceFunc: func() *wgbackend.Device { + return &wgbackend.Device{} }, } @@ -291,10 +291,10 @@ func TestBlockRuleNotAccumulatedOnRepeatedEnableRouting(t *testing.T) { } }, GetDeviceFunc: func() *device.FilteredDevice { - return &device.FilteredDevice{Device: dev} + return &device.FilteredDevice{TUN: dev} }, - GetWGDeviceFunc: func() *wgdevice.Device { - return &wgdevice.Device{} + GetWGDeviceFunc: func() *wgbackend.Device { + return &wgbackend.Device{} }, } @@ -410,10 +410,10 @@ func setupTestManager(t *testing.T) *Manager { } }, GetDeviceFunc: func() *device.FilteredDevice { - return &device.FilteredDevice{Device: dev} + return &device.FilteredDevice{TUN: dev} }, - GetWGDeviceFunc: func() *wgdevice.Device { - return &wgdevice.Device{} + GetWGDeviceFunc: func() *wgbackend.Device { + return &wgbackend.Device{} }, } diff --git a/client/firewall/uspfilter/filter_test.go b/client/firewall/uspfilter/filter_test.go index 280ce9311..973b22537 100644 --- a/client/firewall/uspfilter/filter_test.go +++ b/client/firewall/uspfilter/filter_test.go @@ -14,7 +14,6 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - wgdevice "golang.zx2c4.com/wireguard/device" fw "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" @@ -22,6 +21,7 @@ import ( nbiface "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/netflow" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/shared/management/domain" @@ -34,7 +34,7 @@ type IFaceMock struct { NameFunc func() string SetFilterFunc func(device.PacketFilter) error AddressFunc func() wgaddr.Address - GetWGDeviceFunc func() *wgdevice.Device + GetWGDeviceFunc func() *wgbackend.Device GetDeviceFunc func() *device.FilteredDevice } @@ -45,7 +45,7 @@ func (i *IFaceMock) Name() string { return i.NameFunc() } -func (i *IFaceMock) GetWGDevice() *wgdevice.Device { +func (i *IFaceMock) GetWGDevice() *wgbackend.Device { if i.GetWGDeviceFunc == nil { return nil } diff --git a/client/firewall/uspfilter/forwarder/endpoint.go b/client/firewall/uspfilter/forwarder/endpoint.go index fab776f2a..d7e017203 100644 --- a/client/firewall/uspfilter/forwarder/endpoint.go +++ b/client/firewall/uspfilter/forwarder/endpoint.go @@ -5,12 +5,12 @@ import ( "strconv" "sync/atomic" - wgdevice "golang.zx2c4.com/wireguard/device" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" "gvisor.dev/gvisor/pkg/tcpip/stack" nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // PacketCapture captures raw packets for debugging. Implementations must be @@ -23,7 +23,7 @@ type PacketCapture interface { type endpoint struct { logger *nblog.Logger dispatcher stack.NetworkDispatcher - device *wgdevice.Device + device *wgbackend.Device mtu atomic.Uint32 capture atomic.Pointer[PacketCapture] } diff --git a/client/firewall/uspfilter/forwarder/forwarder.go b/client/firewall/uspfilter/forwarder/forwarder.go index 7308b38bd..b63686223 100644 --- a/client/firewall/uspfilter/forwarder/forwarder.go +++ b/client/firewall/uspfilter/forwarder/forwarder.go @@ -12,7 +12,6 @@ import ( "time" log "github.com/sirupsen/logrus" - wgdevice "golang.zx2c4.com/wireguard/device" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" @@ -26,6 +25,7 @@ import ( "github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack" nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" ) @@ -43,7 +43,7 @@ const ( // IFace provides the WireGuard device and overlay addresses the forwarder needs. type IFace interface { - GetWGDevice() *wgdevice.Device + GetWGDevice() *wgbackend.Device Address() wgaddr.Address } diff --git a/client/iface/bind/control.go b/client/iface/bind/control.go index 32b07c330..da495c186 100644 --- a/client/iface/bind/control.go +++ b/client/iface/bind/control.go @@ -1,8 +1,7 @@ package bind import ( - wireguard "golang.zx2c4.com/wireguard/conn" - + "github.com/netbirdio/netbird/client/iface/wgbackend" nbnet "github.com/netbirdio/netbird/client/net" ) @@ -10,6 +9,6 @@ import ( func init() { listener := nbnet.NewListener() if listener.ListenConfig.Control != nil { - *wireguard.ControlFns = append(*wireguard.ControlFns, listener.ListenConfig.Control) + *wgbackend.ControlFns = append(*wgbackend.ControlFns, listener.ListenConfig.Control) } } diff --git a/client/iface/bind/endpoint.go b/client/iface/bind/endpoint.go index caa92f05d..31ffe6da9 100644 --- a/client/iface/bind/endpoint.go +++ b/client/iface/bind/endpoint.go @@ -3,10 +3,10 @@ package bind import ( "net" - wgConn "golang.zx2c4.com/wireguard/conn" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) -type Endpoint = wgConn.StdNetEndpoint +type Endpoint = wgbackend.StdNetEndpoint func EndpointToUDPAddr(e Endpoint) *net.UDPAddr { return &net.UDPAddr{ diff --git a/client/iface/bind/ice_bind.go b/client/iface/bind/ice_bind.go index cd1ed12f1..28f8cec1e 100644 --- a/client/iface/bind/ice_bind.go +++ b/client/iface/bind/ice_bind.go @@ -15,10 +15,10 @@ import ( "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" "golang.org/x/net/ipv6" - wgConn "golang.zx2c4.com/wireguard/conn" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" nbnet "github.com/netbirdio/netbird/client/net" ) @@ -36,7 +36,7 @@ type receiverCreator struct { iceBind *ICEBind } -func (rc receiverCreator) CreateReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxOffload bool, msgPool *sync.Pool) wgConn.ReceiveFunc { +func (rc receiverCreator) CreateReceiverFn(pc wgbackend.BatchReader, conn *net.UDPConn, rxOffload bool, msgPool *sync.Pool) wgbackend.ReceiveFunc { return rc.iceBind.createReceiverFn(pc, conn, rxOffload, msgPool) } @@ -48,7 +48,7 @@ func (rc receiverCreator) CreateReceiverFn(pc wgConn.BatchReader, conn *net.UDPC // without port, in the format of 127.1.x.x where x.x is the last two octets of the peer address. We try to avoid to // use the port because in the Send function the wgConn.Endpoint the port info is not exported. type ICEBind struct { - *wgConn.StdNetBind + *wgbackend.StdNetBind transportNet transport.Net address wgaddr.Address @@ -74,7 +74,7 @@ type ICEBind struct { } func NewICEBind(transportNet transport.Net, address wgaddr.Address, mtu uint16) *ICEBind { - b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind) + b, _ := wgbackend.NewStdNetBind().(*wgbackend.StdNetBind) ib := &ICEBind{ StdNetBind: b, transportNet: transportNet, @@ -90,11 +90,11 @@ func NewICEBind(transportNet transport.Net, address wgaddr.Address, mtu uint16) rc := receiverCreator{ ib, } - ib.StdNetBind = wgConn.NewStdNetBindWithReceiverCreator(rc) + ib.StdNetBind = wgbackend.NewStdNetBindWithReceiverCreator(rc) return ib } -func (s *ICEBind) Open(uport uint16) ([]wgConn.ReceiveFunc, uint16, error) { +func (s *ICEBind) Open(uport uint16) ([]wgbackend.ReceiveFunc, uint16, error) { s.closedChanMu.Lock() defer s.closedChanMu.Unlock() @@ -190,7 +190,7 @@ func (b *ICEBind) ReceiveFromEndpoint(ctx context.Context, ep *Endpoint, buf []b } } -func (b *ICEBind) Send(bufs [][]byte, ep wgConn.Endpoint) error { +func (b *ICEBind) Send(bufs [][]byte, ep wgbackend.Endpoint) error { b.endpointsMu.Lock() conn, ok := b.endpoints[ep.DstIP()] b.endpointsMu.Unlock() @@ -206,7 +206,7 @@ func (b *ICEBind) Send(bufs [][]byte, ep wgConn.Endpoint) error { return nil } -func (s *ICEBind) createReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxOffload bool, msgsPool *sync.Pool) wgConn.ReceiveFunc { +func (s *ICEBind) createReceiverFn(pc wgbackend.BatchReader, conn *net.UDPConn, rxOffload bool, msgsPool *sync.Pool) wgbackend.ReceiveFunc { s.muUDPMux.Lock() defer s.muUDPMux.Unlock() @@ -218,7 +218,7 @@ func (s *ICEBind) createReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxO } s.createOrUpdateMux() - return func(bufs [][]byte, sizes []int, eps []wgConn.Endpoint) (n int, err error) { + return func(bufs [][]byte, sizes []int, eps []wgbackend.Endpoint) (n int, err error) { msgs := getMessages(msgsPool) for i := range bufs { (*msgs)[i].Buffers[0] = bufs[i] @@ -229,13 +229,13 @@ func (s *ICEBind) createReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxO var numMsgs int if runtime.GOOS == "linux" || runtime.GOOS == "android" { if rxOffload { - readAt := len(*msgs) - (wgConn.IdealBatchSize / wgConn.UdpSegmentMaxDatagrams) + readAt := len(*msgs) - (wgbackend.IdealBatchSize / wgbackend.UdpSegmentMaxDatagrams) //nolint:staticcheck _, err = pc.ReadBatch((*msgs)[readAt:], 0) if err != nil { return 0, err } - numMsgs, err = wgConn.SplitCoalescedMessages(*msgs, readAt, wgConn.GetGSOSize) + numMsgs, err = wgbackend.SplitCoalescedMessages(*msgs, readAt, wgbackend.GetGSOSize) if err != nil { return 0, err } @@ -278,8 +278,8 @@ func (s *ICEBind) createReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxO s.activityRecorder.record(addrPort) } - ep := &wgConn.StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation - wgConn.GetSrcFromControl(msg.OOB[:msg.NN], ep) + ep := &wgbackend.StdNetEndpoint{AddrPort: addrPort} // TODO: remove allocation + wgbackend.GetSrcFromControl(msg.OOB[:msg.NN], ep) eps[i] = ep } return numMsgs, nil @@ -363,7 +363,7 @@ func (s *ICEBind) parseSTUNMessage(raw []byte) (*stun.Message, error) { // receiveRelayed is a receive function that is used to receive packets from the relayed connection and forward to the // WireGuard. Critical part is do not block if the Closed() has been called. -func (c *ICEBind) receiveRelayed(buffs [][]byte, sizes []int, eps []wgConn.Endpoint) (int, error) { +func (c *ICEBind) receiveRelayed(buffs [][]byte, sizes []int, eps []wgbackend.Endpoint) (int, error) { closedChan := c.currentClosedChan() select { @@ -375,7 +375,7 @@ func (c *ICEBind) receiveRelayed(buffs [][]byte, sizes []int, eps []wgConn.Endpo } copy(buffs[0], msg.Buffer) sizes[0] = len(msg.Buffer) - eps[0] = wgConn.Endpoint(msg.Endpoint) + eps[0] = wgbackend.Endpoint(msg.Endpoint) if isTransportPkg(buffs, sizes[0]) { if ep, ok := eps[0].(*Endpoint); ok { diff --git a/client/iface/bind/ice_bind_close_test.go b/client/iface/bind/ice_bind_close_test.go index 829d27663..415ca930f 100644 --- a/client/iface/bind/ice_bind_close_test.go +++ b/client/iface/bind/ice_bind_close_test.go @@ -6,7 +6,8 @@ import ( "time" "github.com/stretchr/testify/require" - wgConn "golang.zx2c4.com/wireguard/conn" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // startReceivers runs every receive function the way wireguard-go's device @@ -14,7 +15,7 @@ import ( // calling Bind.Close, closeBindLocked waits on exactly that WaitGroup while // holding device.net, so a receive function that never returns wedges the // device and every goroutine that needs the same lock. -func startReceivers(fns []wgConn.ReceiveFunc) *sync.WaitGroup { +func startReceivers(fns []wgbackend.ReceiveFunc) *sync.WaitGroup { wg, _ := startReceiversEntered(fns) return wg } @@ -23,18 +24,18 @@ func startReceivers(fns []wgConn.ReceiveFunc) *sync.WaitGroup { // function has been called at least once. A receive function that has been // entered is either inside its blocking receive or about to be, which is a // stronger signal to synchronise on than a bare sleep. -func startReceiversEntered(fns []wgConn.ReceiveFunc) (*sync.WaitGroup, <-chan struct{}) { +func startReceiversEntered(fns []wgbackend.ReceiveFunc) (*sync.WaitGroup, <-chan struct{}) { var wg sync.WaitGroup var entered sync.WaitGroup wg.Add(len(fns)) entered.Add(len(fns)) for i := range fns { - go func(fn wgConn.ReceiveFunc) { + go func(fn wgbackend.ReceiveFunc) { defer wg.Done() buffs := [][]byte{make([]byte, 1500)} sizes := make([]int, 1) - eps := make([]wgConn.Endpoint, 1) + eps := make([]wgbackend.Endpoint, 1) first := true for { if first { diff --git a/client/iface/bind/ice_bind_test.go b/client/iface/bind/ice_bind_test.go index 8dd82d7df..25b3337e3 100644 --- a/client/iface/bind/ice_bind_test.go +++ b/client/iface/bind/ice_bind_test.go @@ -16,10 +16,9 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) func TestICEBind_CreatesReceiverForBothIPv4AndIPv6(t *testing.T) { @@ -370,16 +369,16 @@ func TestICEBind_OpenDoesNotDiscoverInterfaces(t *testing.T) { } func TestICEBind_DeviceUpDoesNotWaitOnInterfaceDiscovery(t *testing.T) { - tunDev, _, err := netstack.CreateNetTUN([]netip.Addr{netip.MustParseAddr("100.64.0.1")}, nil, 1280) + tunDev, _, err := wgbackend.CreateNetTUN([]netip.Addr{netip.MustParseAddr("100.64.0.1")}, nil, 1280) require.NoError(t, err) - var dev *device.Device + var dev *wgbackend.Device n := newCountingNet() n.before = func() { _ = dev.IpcGetOperation(io.Discard) } iceBind := NewICEBind(n, testAddress(), 1280) - dev = device.NewDevice(tunDev, iceBind, device.NewLogger(device.LogLevelSilent, "")) + dev = wgbackend.NewDevice(tunDev, iceBind, wgbackend.NewLogger(wgbackend.LogLevelSilent, "")) upDone := make(chan error, 1) go func() { diff --git a/client/iface/bind/relay_bind.go b/client/iface/bind/relay_bind.go index 4c179d6a5..2f962e68e 100644 --- a/client/iface/bind/relay_bind.go +++ b/client/iface/bind/relay_bind.go @@ -7,15 +7,15 @@ import ( "sync" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/conn" "github.com/netbirdio/netbird/client/iface/udpmux" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // RelayBindJS is a conn.Bind implementation for WebAssembly environments. // Do not limit to build only js, because we want to be able to run tests type RelayBindJS struct { - *conn.StdNetBind + *wgbackend.StdNetBind recvChan chan recvMessage endpoints map[netip.Addr]net.Conn @@ -34,12 +34,12 @@ func NewRelayBindJS() *RelayBindJS { } // Open creates a receive function for handling relay packets in WASM. -func (s *RelayBindJS) Open(uport uint16) ([]conn.ReceiveFunc, uint16, error) { +func (s *RelayBindJS) Open(uport uint16) ([]wgbackend.ReceiveFunc, uint16, error) { log.Debugf("Open: creating receive function for port %d", uport) s.ctx, s.cancel = context.WithCancel(context.Background()) - receiveFn := func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (int, error) { + receiveFn := func(bufs [][]byte, sizes []int, eps []wgbackend.Endpoint) (int, error) { select { case <-s.ctx.Done(): return 0, net.ErrClosed @@ -49,13 +49,13 @@ func (s *RelayBindJS) Open(uport uint16) ([]conn.ReceiveFunc, uint16, error) { } copy(bufs[0], msg.Buffer) sizes[0] = len(msg.Buffer) - eps[0] = conn.Endpoint(msg.Endpoint) + eps[0] = wgbackend.Endpoint(msg.Endpoint) return 1, nil } } log.Debugf("Open: receive function created, returning port %d", uport) - return []conn.ReceiveFunc{receiveFn}, uport, nil + return []wgbackend.ReceiveFunc{receiveFn}, uport, nil } func (s *RelayBindJS) Close() error { @@ -78,7 +78,7 @@ func (s *RelayBindJS) ReceiveFromEndpoint(ctx context.Context, ep *Endpoint, buf } // Send forwards packets through the relay connection for WASM. -func (s *RelayBindJS) Send(bufs [][]byte, ep conn.Endpoint) error { +func (s *RelayBindJS) Send(bufs [][]byte, ep wgbackend.Endpoint) error { if ep == nil { return nil } diff --git a/client/iface/bind/stun_filter_test.go b/client/iface/bind/stun_filter_test.go index 0e118e0fd..18befe539 100644 --- a/client/iface/bind/stun_filter_test.go +++ b/client/iface/bind/stun_filter_test.go @@ -12,7 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/ipv4" - wgConn "golang.zx2c4.com/wireguard/conn" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // magicCookieBytes is the STUN magic cookie as it appears on the wire. In a @@ -165,7 +166,7 @@ func TestReceiveFn_ClearsSizeOfConsumedPacket(t *testing.T) { // A leftover size from an earlier read, which is what makes the missing reset // observable. sizes := []int{148} - eps := make([]wgConn.Endpoint, 1) + eps := make([]wgbackend.Endpoint, 1) n, err := recvFn(bufs, sizes, eps) require.NoError(t, err) diff --git a/client/iface/configurer/uapi.go b/client/iface/configurer/uapi.go index d9bd9bfab..231e26879 100644 --- a/client/iface/configurer/uapi.go +++ b/client/iface/configurer/uapi.go @@ -5,16 +5,16 @@ package configurer import ( "net" - "golang.zx2c4.com/wireguard/ipc" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) func openUAPI(deviceName string) (net.Listener, error) { - uapiSock, err := ipc.UAPIOpen(deviceName) + uapiSock, err := wgbackend.UAPIOpen(deviceName) if err != nil { return nil, err } - listener, err := ipc.UAPIListen(deviceName, uapiSock) + listener, err := wgbackend.UAPIListen(deviceName, uapiSock) if err != nil { _ = uapiSock.Close() return nil, err diff --git a/client/iface/configurer/uapi_windows.go b/client/iface/configurer/uapi_windows.go index 46fa90c2e..43d03ea0a 100644 --- a/client/iface/configurer/uapi_windows.go +++ b/client/iface/configurer/uapi_windows.go @@ -3,9 +3,9 @@ package configurer import ( "net" - "golang.zx2c4.com/wireguard/ipc" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) func openUAPI(deviceName string) (net.Listener, error) { - return ipc.UAPIListen(deviceName) + return wgbackend.UAPIListen(deviceName) } diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 1b0965f0d..2b4f73810 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -10,11 +10,10 @@ import ( "time" log "github.com/sirupsen/logrus" - wgconn "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" + "github.com/netbirdio/netbird/client/iface/wgbackend" nbnet "github.com/netbirdio/netbird/client/net" "github.com/netbirdio/netbird/monotime" ) @@ -22,7 +21,7 @@ import ( var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found") type WGUSPConfigurer struct { - device *device.Device + device *wgbackend.Device deviceName string activityRecorder *bind.ActivityRecorder statsCache *statsCache @@ -31,14 +30,14 @@ type WGUSPConfigurer struct { } // NewUSPConfigurer creates a userspace configurer and starts its UAPI listener. -func NewUSPConfigurer(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer { +func NewUSPConfigurer(device *wgbackend.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer { wgCfg := NewUSPConfigurerNoUAPI(device, deviceName, activityRecorder) wgCfg.startUAPI() return wgCfg } // NewUSPConfigurerNoUAPI creates a userspace configurer without a UAPI listener. -func NewUSPConfigurerNoUAPI(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer { +func NewUSPConfigurerNoUAPI(device *wgbackend.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer { wgCfg := &WGUSPConfigurer{ device: device, deviceName: deviceName, @@ -59,7 +58,7 @@ func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error return fmt.Errorf("invalid listen port %d", port) } - if err := c.device.SetPrivateKey(device.NoisePrivateKey(key)); err != nil { + if err := c.device.SetPrivateKey(wgbackend.NoisePrivateKey(key)); err != nil { return fmt.Errorf("set private key: %w", err) } if err := c.device.SetListenPort(uint16(port)); err != nil { @@ -80,8 +79,8 @@ func (c *WGUSPConfigurer) SetPresharedKey(peerKey string, psk wgtypes.Key, updat return err } - presharedKey := device.NoisePresharedKey(psk) - return c.device.ConfigurePeer(device.NoisePublicKey(parsedPeerKey), device.PeerConfig{ + presharedKey := wgbackend.NoisePresharedKey(psk) + return c.device.ConfigurePeer(wgbackend.NoisePublicKey(parsedPeerKey), wgbackend.PeerConfig{ PresharedKey: &presharedKey, UpdateOnly: updateOnly, }) @@ -99,7 +98,7 @@ func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix, return err } - cfg := device.PeerConfig{ + cfg := wgbackend.PeerConfig{ PersistentKeepalive: &keepAlive, AllowedIPs: prefixes, } @@ -116,11 +115,11 @@ func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix, } if preSharedKey != nil { - psk := device.NoisePresharedKey(*preSharedKey) + psk := wgbackend.NoisePresharedKey(*preSharedKey) cfg.PresharedKey = &psk } - if err := c.device.ConfigurePeer(device.NoisePublicKey(peerKeyParsed), cfg); err != nil { + if err := c.device.ConfigurePeer(wgbackend.NoisePublicKey(peerKeyParsed), cfg); err != nil { return fmt.Errorf("configure peer: %w", err) } @@ -138,7 +137,7 @@ func (c *WGUSPConfigurer) RemoveEndpointAddress(peerKey string) error { return fmt.Errorf("parse peer key: %w", err) } - peer := c.device.LookupPeer(device.NoisePublicKey(peerKeyParsed)) + peer := c.device.LookupPeer(wgbackend.NoisePublicKey(peerKeyParsed)) if peer == nil { return ErrPeerNotFound } @@ -147,7 +146,7 @@ func (c *WGUSPConfigurer) RemoveEndpointAddress(peerKey string) error { peer.ZeroAndFlushAll() var noKeepAlive time.Duration - if err := c.device.ConfigurePeer(device.NoisePublicKey(peerKeyParsed), device.PeerConfig{ + if err := c.device.ConfigurePeer(wgbackend.NoisePublicKey(peerKeyParsed), wgbackend.PeerConfig{ PersistentKeepalive: &noKeepAlive, UpdateOnly: true, }); err != nil { @@ -163,7 +162,7 @@ func (c *WGUSPConfigurer) RemovePeer(peerKey string) error { return err } - c.device.RemovePeer(device.NoisePublicKey(peerKeyParsed)) + c.device.RemovePeer(wgbackend.NoisePublicKey(peerKeyParsed)) c.activityRecorder.Remove(peerKey) return nil } @@ -178,7 +177,7 @@ func (c *WGUSPConfigurer) AddAllowedIP(peerKey string, allowedIP netip.Prefix) e return fmt.Errorf("invalid allowed IP %v", allowedIP) } - peer := c.device.LookupPeer(device.NoisePublicKey(peerKeyParsed)) + peer := c.device.LookupPeer(wgbackend.NoisePublicKey(peerKeyParsed)) if peer == nil { return nil } @@ -198,7 +197,7 @@ func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix return ErrAllowedIPNotFound } - peer := c.device.LookupPeer(device.NoisePublicKey(peerKeyParsed)) + peer := c.device.LookupPeer(wgbackend.NoisePublicKey(peerKeyParsed)) if peer == nil { return ErrPeerNotFound } @@ -284,7 +283,7 @@ func (c *WGUSPConfigurer) fetchStats() (map[string]WGStats, error) { return stats, nil } -func peerStats(peer *device.Peer) Peer { +func peerStats(peer *wgbackend.Peer) Peer { stats := Peer{ PublicKey: wgtypes.Key(peer.PublicKey()).String(), AllowedIPs: prefixesToIPNets(peer.AllowedIPs()), @@ -299,7 +298,7 @@ func peerStats(peer *device.Peer) Peer { return stats } -func endpointAddrPort(endpoint wgconn.Endpoint) (netip.AddrPort, bool) { +func endpointAddrPort(endpoint wgbackend.Endpoint) (netip.AddrPort, bool) { if endpoint == nil { return netip.AddrPort{}, false } diff --git a/client/iface/configurer/usp_allowedips_test.go b/client/iface/configurer/usp_allowedips_test.go index effb360e9..81bd26553 100644 --- a/client/iface/configurer/usp_allowedips_test.go +++ b/client/iface/configurer/usp_allowedips_test.go @@ -8,11 +8,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - wgconn "golang.zx2c4.com/wireguard/conn" - wgdevice "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/bind" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // newTestUSPConfigurer builds a configurer over a real wireguard-go device backed by an @@ -20,7 +19,7 @@ import ( func newTestUSPConfigurer(t *testing.T) *WGUSPConfigurer { t.Helper() - dev := wgdevice.NewDevice(newIdleTUN(), wgconn.NewDefaultBind(), wgdevice.NewLogger(wgdevice.LogLevelSilent, "")) + dev := wgbackend.NewDevice(newIdleTUN(), wgbackend.NewDefaultBind(), wgbackend.NewLogger(wgbackend.LogLevelSilent, "")) t.Cleanup(dev.Close) c := NewUSPConfigurerNoUAPI(dev, "wgtest0", bind.NewActivityRecorder()) diff --git a/client/iface/configurer/usp_test.go b/client/iface/configurer/usp_test.go index 48b5ad1f0..1ea406a1e 100644 --- a/client/iface/configurer/usp_test.go +++ b/client/iface/configurer/usp_test.go @@ -11,20 +11,20 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - wgdevice "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // idleTUN never reports an up event, so the wireguard-go device stays down: no socket is // opened, no peer goroutine starts, and the configuration state is all the tests observe. type idleTUN struct { - events chan tun.Event + events chan wgbackend.TUNEvent closed chan struct{} } func newIdleTUN() *idleTUN { - return &idleTUN{events: make(chan tun.Event), closed: make(chan struct{})} + return &idleTUN{events: make(chan wgbackend.TUNEvent), closed: make(chan struct{})} } func (t *idleTUN) File() *os.File { return nil } @@ -40,7 +40,7 @@ func (t *idleTUN) MTU() (int, error) { return 1420, nil } func (t *idleTUN) Name() (string, error) { return "idle", nil } -func (t *idleTUN) Events() <-chan tun.Event { return t.events } +func (t *idleTUN) Events() <-chan wgbackend.TUNEvent { return t.events } func (t *idleTUN) Close() error { close(t.closed) @@ -142,12 +142,12 @@ func TestRemoveEndpointAddressKeepsThePeerAndStopsTheKeepalive(t *testing.T) { endpoint := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 10), Port: 51820} prefixes := []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")} require.NoError(t, c.UpdatePeer(peerKey, prefixes, 25*time.Second, endpoint, nil), "add peer") - before := c.device.LookupPeer(wgdevice.NoisePublicKey(priv.PublicKey())) + before := c.device.LookupPeer(wgbackend.NoisePublicKey(priv.PublicKey())) require.NotNil(t, before, "the peer must be on the device") require.NoError(t, c.RemoveEndpointAddress(peerKey), "remove endpoint address") - assert.Same(t, before, c.device.LookupPeer(wgdevice.NoisePublicKey(priv.PublicKey())), + assert.Same(t, before, c.device.LookupPeer(wgbackend.NoisePublicKey(priv.PublicKey())), "the peer object must survive the endpoint removal") stats, err := c.FullStats() require.NoError(t, err, "read device stats") diff --git a/client/iface/device.go b/client/iface/device.go index c0c829825..664093abd 100644 --- a/client/iface/device.go +++ b/client/iface/device.go @@ -3,13 +3,10 @@ package iface import ( - "golang.zx2c4.com/wireguard/tun/netstack" - - wgdevice "golang.zx2c4.com/wireguard/device" - "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type WGTunDevice interface { @@ -21,7 +18,7 @@ type WGTunDevice interface { DeviceName() string Close() error FilteredDevice() *device.FilteredDevice - Device() *wgdevice.Device - GetNet() *netstack.Net + Device() *wgbackend.Device + GetNet() *wgbackend.NetstackNet GetICEBind() device.EndpointManager } diff --git a/client/iface/device/device_android.go b/client/iface/device/device_android.go index 0ed1299ae..0c6b1c03c 100644 --- a/client/iface/device/device_android.go +++ b/client/iface/device/device_android.go @@ -8,14 +8,12 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // WGTunDevice ignore the WGTunDevice interface on Android because the creation of the tun device is different on this platform @@ -30,7 +28,7 @@ type WGTunDevice struct { disableDNS bool name string - device *device.Device + device *wgbackend.Device filteredDevice *FilteredDevice udpMux *udpmux.UniversalUDPMuxDefault configurer WGConfigurer @@ -74,7 +72,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string return nil, err } - unmonitoredTUN, name, err := tun.CreateUnmonitoredTUNFromFD(fd) + unmonitoredTUN, name, err := wgbackend.CreateUnmonitoredTUNFromFD(fd) if err != nil { _ = unix.Close(fd) log.Errorf("failed to create Android interface: %s", err) @@ -87,7 +85,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string t.filteredDevice = newDeviceFilter(t.renewableTun) log.Debugf("attaching to interface %v", name) - t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] ")) + t.device = wgbackend.NewDevice(t.filteredDevice, t.iceBind, wgbackend.NewLogger(wgLogLevel(), "[netbird] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() @@ -121,7 +119,7 @@ func (t *WGTunDevice) RenewTun(fd int) error { return fmt.Errorf("device not initialized") } - unmonitoredTUN, _, err := tun.CreateUnmonitoredTUNFromFD(fd) + unmonitoredTUN, _, err := wgbackend.CreateUnmonitoredTUNFromFD(fd) if err != nil { _ = unix.Close(fd) log.Errorf("failed to renew Android interface: %s", err) @@ -155,7 +153,7 @@ func (t *WGTunDevice) Close() error { return nil } -func (t *WGTunDevice) Device() *device.Device { +func (t *WGTunDevice) Device() *wgbackend.Device { return t.device } @@ -175,7 +173,7 @@ func (t *WGTunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } -func (t *WGTunDevice) GetNet() *netstack.Net { +func (t *WGTunDevice) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/iface/device/device_darwin.go b/client/iface/device/device_darwin.go index ac8f8a51b..cf03c12fe 100644 --- a/client/iface/device/device_darwin.go +++ b/client/iface/device/device_darwin.go @@ -7,14 +7,12 @@ import ( "os/exec" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type TunDevice struct { @@ -25,7 +23,7 @@ type TunDevice struct { mtu uint16 iceBind *bind.ICEBind - device *device.Device + device *wgbackend.Device filteredDevice *FilteredDevice udpMux *udpmux.UniversalUDPMuxDefault configurer WGConfigurer @@ -43,17 +41,17 @@ func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu } func (t *TunDevice) Create() (WGConfigurer, error) { - tunDevice, err := tun.CreateTUN(t.name, int(t.mtu)) + tunDevice, err := wgbackend.CreateTUN(t.name, int(t.mtu)) if err != nil { return nil, fmt.Errorf("error creating tun device: %s", err) } t.filteredDevice = newDeviceFilter(tunDevice) // We need to create a wireguard-go device and listen to configuration requests - t.device = device.NewDevice( + t.device = wgbackend.NewDevice( t.filteredDevice, t.iceBind, - device.NewLogger(wgLogLevel(), "[netbird] "), + wgbackend.NewLogger(wgLogLevel(), "[netbird] "), ) err = t.assignAddr() @@ -125,7 +123,7 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { } // Device returns the wireguard device -func (t *TunDevice) Device() *device.Device { +func (t *TunDevice) Device() *wgbackend.Device { return t.device } @@ -160,7 +158,7 @@ func (t *TunDevice) assignAddr() error { return nil } -func (t *TunDevice) GetNet() *netstack.Net { +func (t *TunDevice) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/iface/device/device_filter.go b/client/iface/device/device_filter.go index 7d7493835..d922af5fa 100644 --- a/client/iface/device/device_filter.go +++ b/client/iface/device/device_filter.go @@ -8,7 +8,8 @@ import ( "sync/atomic" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // PacketFilter interface for firewall abilities @@ -42,7 +43,7 @@ type PacketCapture interface { // FilteredDevice to override Read or Write of packets type FilteredDevice struct { - tun.Device + wgbackend.TUN filter PacketFilter capture atomic.Pointer[PacketCapture] @@ -54,9 +55,9 @@ type FilteredDevice struct { } // newDeviceFilter constructor function -func newDeviceFilter(device tun.Device) *FilteredDevice { +func newDeviceFilter(device wgbackend.TUN) *FilteredDevice { return &FilteredDevice{ - Device: device, + TUN: device, } } @@ -66,7 +67,7 @@ func newDeviceFilter(device tun.Device) *FilteredDevice { func (d *FilteredDevice) Close() error { var err error d.closeOnce.Do(func() { - err = d.Device.Close() + err = d.TUN.Close() }) if err != nil { return err @@ -142,14 +143,14 @@ func (d *FilteredDevice) Write(bufs [][]byte, offset int) (int, error) { // wintun read path and converting them into errors. func (d *FilteredDevice) deviceRead(bufs [][]byte, sizes []int, offset int) (n int, err error) { defer d.recoverFromPanic("read", &n, &err) - return d.Device.Read(bufs, sizes, offset) + return d.TUN.Read(bufs, sizes, offset) } // deviceWrite calls the underlying device Write, recovering from panics in the // wintun write path and converting them into errors. func (d *FilteredDevice) deviceWrite(bufs [][]byte, offset int) (n int, err error) { defer d.recoverFromPanic("write", &n, &err) - return d.Device.Write(bufs, offset) + return d.TUN.Write(bufs, offset) } // recoverFromPanic converts a panic in the underlying device into a regular diff --git a/client/iface/device/device_ios.go b/client/iface/device/device_ios.go index 8368c8dce..df51c0e15 100644 --- a/client/iface/device/device_ios.go +++ b/client/iface/device/device_ios.go @@ -6,14 +6,12 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type TunDevice struct { @@ -25,7 +23,7 @@ type TunDevice struct { iceBind *bind.ICEBind tunFd int - device *device.Device + device *wgbackend.Device filteredDevice *FilteredDevice udpMux *udpmux.UniversalUDPMuxDefault configurer WGConfigurer @@ -50,7 +48,7 @@ var ErrInvalidTunnelFD = fmt.Errorf("invalid tunnel file descriptor: fd is 0 (Sw func (t *TunDevice) Create() (WGConfigurer, error) { log.Infof("create tun interface") - var tunDevice tun.Device + var tunDevice wgbackend.TUN var err error // Validate the tunnel file descriptor. @@ -79,7 +77,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { _ = unix.Close(dupTunFd) return nil, err } - tunDevice, err = tun.CreateTUNFromFile(os.NewFile(uintptr(dupTunFd), "/dev/tun"), 0) + tunDevice, err = wgbackend.CreateTUNFromFile(os.NewFile(uintptr(dupTunFd), "/dev/tun"), 0) if err != nil { log.Errorf("Unable to create new tun device from fd: %v", err) _ = unix.Close(dupTunFd) @@ -88,7 +86,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) { t.filteredDevice = newDeviceFilter(tunDevice) log.Debug("Attaching to interface") - t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] ")) + t.device = wgbackend.NewDevice(t.filteredDevice, t.iceBind, wgbackend.NewLogger(wgLogLevel(), "[netbird] ")) // without this property mobile devices can discover remote endpoints if the configured one was wrong. // this helps with support for the older NetBird clients that had a hardcoded direct mode // t.device.DisableSomeRoamingForBrokenMobileSemantics() @@ -118,7 +116,7 @@ func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) { return udpMux, nil } -func (t *TunDevice) Device() *device.Device { +func (t *TunDevice) Device() *wgbackend.Device { return t.device } @@ -163,7 +161,7 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { return t.filteredDevice } -func (t *TunDevice) GetNet() *netstack.Net { +func (t *TunDevice) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/iface/device/device_kernel_unix.go b/client/iface/device/device_kernel_unix.go index ee965dd24..240154f81 100644 --- a/client/iface/device/device_kernel_unix.go +++ b/client/iface/device/device_kernel_unix.go @@ -9,12 +9,11 @@ import ( "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" nbnet "github.com/netbirdio/netbird/client/net" "github.com/netbirdio/netbird/sharedsock" ) @@ -170,7 +169,7 @@ func (t *TunKernelDevice) DeviceName() string { } // Device returns the wireguard device, not applicable for kernel devices -func (t *TunKernelDevice) Device() *device.Device { +func (t *TunKernelDevice) Device() *wgbackend.Device { return nil } @@ -183,7 +182,7 @@ func (t *TunKernelDevice) assignAddr() error { return t.link.assignAddr(&t.address) } -func (t *TunKernelDevice) GetNet() *netstack.Net { +func (t *TunKernelDevice) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go index b3bce3925..69e0d6bba 100644 --- a/client/iface/device/device_netstack.go +++ b/client/iface/device/device_netstack.go @@ -6,20 +6,18 @@ import ( "net/netip" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" nbnet "github.com/netbirdio/netbird/client/net" ) type Bind interface { - conn.Bind + wgbackend.Bind GetICEMux() (*udpmux.UniversalUDPMuxDefault, error) ActivityRecorder() *bind.ActivityRecorder EndpointManager @@ -34,13 +32,13 @@ type TunNetstackDevice struct { listenAddress string bind Bind - device *device.Device + device *wgbackend.Device filteredDevice *FilteredDevice nsTun *nbnetstack.NetStackTun udpMux *udpmux.UniversalUDPMuxDefault configurer WGConfigurer - net *netstack.Net + net *wgbackend.NetstackNet } func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, bind Bind, listenAddress string) *TunNetstackDevice { @@ -78,10 +76,10 @@ func (t *TunNetstackDevice) create() (WGConfigurer, error) { t.filteredDevice = newDeviceFilter(tunIface) t.net = net - t.device = device.NewDevice( + t.device = wgbackend.NewDevice( t.filteredDevice, t.bind, - device.NewLogger(wgLogLevel(), "[netbird] "), + wgbackend.NewLogger(wgLogLevel(), "[netbird] "), ) t.configurer = configurer.NewUSPConfigurerNoUAPI(t.device, t.name, t.bind.ActivityRecorder()) @@ -156,11 +154,11 @@ func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice { } // Device returns the wireguard device -func (t *TunNetstackDevice) Device() *device.Device { +func (t *TunNetstackDevice) Device() *wgbackend.Device { return t.device } -func (t *TunNetstackDevice) GetNet() *netstack.Net { +func (t *TunNetstackDevice) GetNet() *wgbackend.NetstackNet { return t.net } diff --git a/client/iface/device/device_usp_unix.go b/client/iface/device/device_usp_unix.go index 04c265c49..2b5f0a2b7 100644 --- a/client/iface/device/device_usp_unix.go +++ b/client/iface/device/device_usp_unix.go @@ -6,14 +6,12 @@ import ( "fmt" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type TunDevice struct { @@ -24,7 +22,7 @@ type TunDevice struct { mtu uint16 iceBind *bind.ICEBind - device *device.Device + device *wgbackend.Device filteredDevice *FilteredDevice udpMux *udpmux.UniversalUDPMuxDefault configurer WGConfigurer @@ -45,7 +43,7 @@ func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu func (t *TunDevice) Create() (WGConfigurer, error) { log.Info("create tun interface") - tunIface, err := tun.CreateTUN(t.name, int(t.mtu)) + tunIface, err := wgbackend.CreateTUN(t.name, int(t.mtu)) if err != nil { log.Debugf("failed to create tun interface (%s, %d): %s", t.name, int(t.mtu), err) return nil, fmt.Errorf("error creating tun device: %s", err) @@ -53,10 +51,10 @@ func (t *TunDevice) Create() (WGConfigurer, error) { t.filteredDevice = newDeviceFilter(tunIface) // We need to create a wireguard-go device and listen to configuration requests - t.device = device.NewDevice( + t.device = wgbackend.NewDevice( t.filteredDevice, t.iceBind, - device.NewLogger(wgLogLevel(), "[netbird] "), + wgbackend.NewLogger(wgLogLevel(), "[netbird] "), ) err = t.assignAddr() @@ -132,7 +130,7 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { } // Device returns the wireguard device -func (t *TunDevice) Device() *device.Device { +func (t *TunDevice) Device() *wgbackend.Device { return t.device } @@ -143,7 +141,7 @@ func (t *TunDevice) assignAddr() error { return link.assignAddr(&t.address) } -func (t *TunDevice) GetNet() *netstack.Net { +func (t *TunDevice) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/iface/device/device_windows.go b/client/iface/device/device_windows.go index f52392fa2..4e9dd1eac 100644 --- a/client/iface/device/device_windows.go +++ b/client/iface/device/device_windows.go @@ -6,15 +6,13 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) const defaultWindowsGUIDSTring = "{f2f29e61-d91f-4d76-8151-119b20c4bdeb}" @@ -27,8 +25,8 @@ type TunDevice struct { mtu uint16 iceBind *bind.ICEBind - device *device.Device - nativeTunDevice *tun.NativeTun + device *wgbackend.Device + nativeTunDevice *wgbackend.NativeTUN filteredDevice *FilteredDevice udpMux *udpmux.UniversalUDPMuxDefault configurer WGConfigurer @@ -60,18 +58,18 @@ func (t *TunDevice) Create() (WGConfigurer, error) { return nil, err } log.Info("create tun interface") - tunDevice, err := tun.CreateTUNWithRequestedGUID(t.name, &guid, int(t.mtu)) + tunDevice, err := wgbackend.CreateTUNWithRequestedGUID(t.name, &guid, int(t.mtu)) if err != nil { return nil, fmt.Errorf("error creating tun device: %s", err) } - t.nativeTunDevice = tunDevice.(*tun.NativeTun) + t.nativeTunDevice = tunDevice.(*wgbackend.NativeTUN) t.filteredDevice = newDeviceFilter(tunDevice) // We need to create a wireguard-go device and listen to configuration requests - t.device = device.NewDevice( + t.device = wgbackend.NewDevice( t.filteredDevice, t.iceBind, - device.NewLogger(wgLogLevel(), "[netbird] "), + wgbackend.NewLogger(wgLogLevel(), "[netbird] "), ) luid := winipcfg.LUID(t.nativeTunDevice.LUID()) @@ -172,7 +170,7 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice { } // Device returns the wireguard device -func (t *TunDevice) Device() *device.Device { +func (t *TunDevice) Device() *wgbackend.Device { return t.device } @@ -209,7 +207,7 @@ func (t *TunDevice) assignAddr() error { return luid.SetIPAddresses([]netip.Prefix{v4Prefix}) } -func (t *TunDevice) GetNet() *netstack.Net { +func (t *TunDevice) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/iface/device/renewable_tun.go b/client/iface/device/renewable_tun.go index a501eebbb..222dc40f5 100644 --- a/client/iface/device/renewable_tun.go +++ b/client/iface/device/renewable_tun.go @@ -10,7 +10,8 @@ import ( "time" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // closeAwareDevice wraps a tun.Device along with a flag @@ -23,15 +24,15 @@ import ( // goroutine is awaited and closed only once. type closeAwareDevice struct { isClosed atomic.Bool - tun.Device + wgbackend.TUN closeEventCh chan struct{} wg sync.WaitGroup closeOnce sync.Once } -func newClosableDevice(tunDevice tun.Device) *closeAwareDevice { +func newClosableDevice(tunDevice wgbackend.TUN) *closeAwareDevice { return &closeAwareDevice{ - Device: tunDevice, + TUN: tunDevice, isClosed: atomic.Bool{}, closeEventCh: make(chan struct{}), } @@ -39,18 +40,18 @@ func newClosableDevice(tunDevice tun.Device) *closeAwareDevice { // redirectEvents redirects the Events() method of the underlying tun.Device // to the given channel (RenewableTUN's events channel). -func (c *closeAwareDevice) redirectEvents(out chan tun.Event) { +func (c *closeAwareDevice) redirectEvents(out chan wgbackend.TUNEvent) { c.wg.Add(1) go func() { defer c.wg.Done() for { select { - case ev, ok := <-c.Device.Events(): + case ev, ok := <-c.TUN.Events(): if !ok { return } - if ev == tun.EventDown { + if ev == wgbackend.TUNEventDown { continue } @@ -72,7 +73,7 @@ func (c *closeAwareDevice) Close() (err error) { c.closeOnce.Do(func() { c.isClosed.Store(true) close(c.closeEventCh) - err = c.Device.Close() + err = c.TUN.Close() c.wg.Wait() }) @@ -87,7 +88,7 @@ type RenewableTUN struct { devices []*closeAwareDevice mu sync.Mutex cond *sync.Cond - events chan tun.Event + events chan wgbackend.TUNEvent closed atomic.Bool } @@ -95,7 +96,7 @@ func NewRenewableTUN() *RenewableTUN { r := &RenewableTUN{ devices: make([]*closeAwareDevice, 0), mu: sync.Mutex{}, - events: make(chan tun.Event, 16), + events: make(chan wgbackend.TUNEvent, 16), } r.cond = sync.NewCond(&r.mu) return r @@ -225,7 +226,7 @@ func (r *RenewableTUN) Name() (string, error) { // Events returns a channel that is fed events from the underlying tun.Device's events channel // once it is added. -func (r *RenewableTUN) Events() <-chan tun.Event { +func (r *RenewableTUN) Events() <-chan wgbackend.TUNEvent { return r.events } @@ -259,7 +260,7 @@ func (r *RenewableTUN) BatchSize() int { return 1 } -func (r *RenewableTUN) AddDevice(device tun.Device) { +func (r *RenewableTUN) AddDevice(device wgbackend.TUN) { r.mu.Lock() if r.closed.Load() { r.mu.Unlock() diff --git a/client/iface/device/wg_log.go b/client/iface/device/wg_log.go index db2f3111f..107d739ec 100644 --- a/client/iface/device/wg_log.go +++ b/client/iface/device/wg_log.go @@ -3,13 +3,13 @@ package device import ( "os" - "golang.zx2c4.com/wireguard/device" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) func wgLogLevel() int { if os.Getenv("NB_WG_DEBUG") == "true" { - return device.LogLevelVerbose + return wgbackend.LogLevelVerbose } else { - return device.LogLevelSilent + return wgbackend.LogLevelSilent } } diff --git a/client/iface/device_android.go b/client/iface/device_android.go index 3899bf426..c012b9c64 100644 --- a/client/iface/device_android.go +++ b/client/iface/device_android.go @@ -1,13 +1,10 @@ package iface import ( - wgdevice "golang.zx2c4.com/wireguard/device" - - "golang.zx2c4.com/wireguard/tun/netstack" - "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type WGTunDevice interface { @@ -19,8 +16,8 @@ type WGTunDevice interface { DeviceName() string Close() error FilteredDevice() *device.FilteredDevice - Device() *wgdevice.Device - GetNet() *netstack.Net + Device() *wgbackend.Device + GetNet() *wgbackend.NetstackNet RenewTun(fd int) error GetICEBind() device.EndpointManager } diff --git a/client/iface/iface.go b/client/iface/iface.go index f6006fa87..d241d2146 100644 --- a/client/iface/iface.go +++ b/client/iface/iface.go @@ -10,17 +10,15 @@ import ( "github.com/hashicorp/go-multierror" "github.com/pion/transport/v3" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - wgdevice "golang.zx2c4.com/wireguard/device" - "github.com/netbirdio/netbird/client/errors" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/monotime" ) @@ -271,7 +269,7 @@ func (w *WGIface) GetDevice() *device.FilteredDevice { } // GetWGDevice returns the WireGuard device -func (w *WGIface) GetWGDevice() *wgdevice.Device { +func (w *WGIface) GetWGDevice() *wgbackend.Device { return w.tun.Device() } @@ -344,7 +342,7 @@ func (w *WGIface) waitUntilRemoved() error { } // GetNet returns the netstack.Net for the netstack device -func (w *WGIface) GetNet() *netstack.Net { +func (w *WGIface) GetNet() *wgbackend.NetstackNet { w.mu.Lock() defer w.mu.Unlock() diff --git a/client/iface/iface_close_test.go b/client/iface/iface_close_test.go index ecbbf4866..e4b267b70 100644 --- a/client/iface/iface_close_test.go +++ b/client/iface/iface_close_test.go @@ -8,13 +8,11 @@ import ( "testing" "time" - wgdevice "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" - "github.com/netbirdio/netbird/client/iface/device" nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/iface/wgproxy" ) @@ -40,8 +38,8 @@ func (f *fakeTunDevice) WgAddress() wgaddr.Address { return wgaddr. func (f *fakeTunDevice) MTU() uint16 { return DefaultMTU } func (f *fakeTunDevice) DeviceName() string { return "nb-close-test" } func (f *fakeTunDevice) FilteredDevice() *device.FilteredDevice { return nil } -func (f *fakeTunDevice) Device() *wgdevice.Device { return nil } -func (f *fakeTunDevice) GetNet() *netstack.Net { return nil } +func (f *fakeTunDevice) Device() *wgbackend.Device { return nil } +func (f *fakeTunDevice) GetNet() *wgbackend.NetstackNet { return nil } func (f *fakeTunDevice) GetICEBind() device.EndpointManager { return nil } func (f *fakeTunDevice) Close() error { diff --git a/client/iface/mocks/tun.go b/client/iface/mocks/tun.go index 519ee6005..124e6a5cd 100644 --- a/client/iface/mocks/tun.go +++ b/client/iface/mocks/tun.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: golang.zx2c4.com/wireguard/tun (interfaces: Device) +// Source: github.com/netbirdio/wireguard-go/tun (interfaces: Device) // Package mocks is a generated GoMock package. package mocks @@ -8,8 +8,8 @@ import ( os "os" reflect "reflect" + tun "github.com/netbirdio/wireguard-go/tun" gomock "go.uber.org/mock/gomock" - tun "golang.zx2c4.com/wireguard/tun" ) // MockDevice is a mock of Device interface. diff --git a/client/iface/netstack/dialer.go b/client/iface/netstack/dialer.go index a9ed6d45b..a09d03545 100644 --- a/client/iface/netstack/dialer.go +++ b/client/iface/netstack/dialer.go @@ -5,7 +5,8 @@ import ( "net" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun/netstack" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type Dialer interface { @@ -13,10 +14,10 @@ type Dialer interface { } type NSDialer struct { - net *netstack.Net + net *wgbackend.NetstackNet } -func NewNSDialer(net *netstack.Net) *NSDialer { +func NewNSDialer(net *wgbackend.NetstackNet) *NSDialer { return &NSDialer{ net: net, } diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go index 8c7526bbb..1c8b02483 100644 --- a/client/iface/netstack/tun.go +++ b/client/iface/netstack/tun.go @@ -6,8 +6,8 @@ import ( "strconv" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun" - "golang.zx2c4.com/wireguard/tun/netstack" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY" @@ -19,7 +19,7 @@ type NetStackTun struct { //nolint:revive listenAddress string proxy *Proxy - tundev tun.Device + tundev wgbackend.TUN } func NewNetStackTun(listenAddress string, addresses []netip.Addr, dnsAddress netip.Addr, mtu int) *NetStackTun { @@ -31,8 +31,8 @@ func NewNetStackTun(listenAddress string, addresses []netip.Addr, dnsAddress net } } -func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) { - nsTunDev, tunNet, err := netstack.CreateNetTUN( +func (t *NetStackTun) Create() (wgbackend.TUN, *wgbackend.NetstackNet, error) { + nsTunDev, tunNet, err := wgbackend.CreateNetTUN( t.addresses, []netip.Addr{t.dnsAddress}, t.mtu) diff --git a/client/iface/udpmux/mux.go b/client/iface/udpmux/mux.go index 2f7287b53..f39aef2ad 100644 --- a/client/iface/udpmux/mux.go +++ b/client/iface/udpmux/mux.go @@ -9,7 +9,7 @@ import ( "strings" "sync" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" "github.com/pion/logging" "github.com/pion/stun/v3" "github.com/pion/transport/v3" diff --git a/client/iface/wgbackend/conn.go b/client/iface/wgbackend/conn.go new file mode 100644 index 000000000..cd33c3102 --- /dev/null +++ b/client/iface/wgbackend/conn.go @@ -0,0 +1,27 @@ +package wgbackend + +import "github.com/netbirdio/wireguard-go/conn" + +type ( + BatchReader = conn.BatchReader + Bind = conn.Bind + Endpoint = conn.Endpoint + ReceiveFunc = conn.ReceiveFunc + StdNetBind = conn.StdNetBind + StdNetEndpoint = conn.StdNetEndpoint +) + +const ( + IdealBatchSize = conn.IdealBatchSize + UdpSegmentMaxDatagrams = conn.UdpSegmentMaxDatagrams +) + +var ( + ControlFns = conn.ControlFns + GetGSOSize = conn.GetGSOSize + GetSrcFromControl = conn.GetSrcFromControl + NewDefaultBind = conn.NewDefaultBind + NewStdNetBind = conn.NewStdNetBind + NewStdNetBindWithReceiverCreator = conn.NewStdNetBindWithReceiverCreator + SplitCoalescedMessages = conn.SplitCoalescedMessages +) diff --git a/client/iface/wgbackend/device.go b/client/iface/wgbackend/device.go new file mode 100644 index 000000000..4fe5048df --- /dev/null +++ b/client/iface/wgbackend/device.go @@ -0,0 +1,24 @@ +package wgbackend + +import "github.com/netbirdio/wireguard-go/device" + +type ( + Device = device.Device + NoisePresharedKey = device.NoisePresharedKey + NoisePrivateKey = device.NoisePrivateKey + NoisePublicKey = device.NoisePublicKey + Peer = device.Peer + PeerConfig = device.PeerConfig +) + +const ( + LogLevelSilent = device.LogLevelSilent + LogLevelVerbose = device.LogLevelVerbose +) + +var ( + NewDevice = device.NewDevice + NewLogger = device.NewLogger + SetMaxBatchSizeOverride = device.SetMaxBatchSizeOverride + SetPreallocatedBuffersPerPool = device.SetPreallocatedBuffersPerPool +) diff --git a/client/iface/wgbackend/doc.go b/client/iface/wgbackend/doc.go new file mode 100644 index 000000000..3274f40f6 --- /dev/null +++ b/client/iface/wgbackend/doc.go @@ -0,0 +1,5 @@ +// Package wgbackend is the single place in the client's hand-written code that names +// the WireGuard implementation. Other packages import it instead of the wireguard-go +// packages, so swapping the implementation only touches the aliases here and the +// generated mocks. +package wgbackend diff --git a/client/iface/wgbackend/ipc_unix.go b/client/iface/wgbackend/ipc_unix.go new file mode 100644 index 000000000..b7f191e02 --- /dev/null +++ b/client/iface/wgbackend/ipc_unix.go @@ -0,0 +1,10 @@ +//go:build linux || darwin || freebsd || openbsd + +package wgbackend + +import "github.com/netbirdio/wireguard-go/ipc" + +var ( + UAPIListen = ipc.UAPIListen + UAPIOpen = ipc.UAPIOpen +) diff --git a/client/iface/wgbackend/ipc_windows.go b/client/iface/wgbackend/ipc_windows.go new file mode 100644 index 000000000..4d0aa941e --- /dev/null +++ b/client/iface/wgbackend/ipc_windows.go @@ -0,0 +1,5 @@ +package wgbackend + +import "github.com/netbirdio/wireguard-go/ipc" + +var UAPIListen = ipc.UAPIListen diff --git a/client/iface/wgbackend/tun.go b/client/iface/wgbackend/tun.go new file mode 100644 index 000000000..f5df72536 --- /dev/null +++ b/client/iface/wgbackend/tun.go @@ -0,0 +1,19 @@ +package wgbackend + +import ( + "github.com/netbirdio/wireguard-go/tun" + "github.com/netbirdio/wireguard-go/tun/netstack" +) + +type ( + // TUN is the packet device the WireGuard device reads from and writes to. + TUN = tun.Device + // TUNEvent is a status change reported by a TUN. + TUNEvent = tun.Event + // NetstackNet is the userspace network stack a netstack TUN is attached to. + NetstackNet = netstack.Net +) + +const TUNEventDown = tun.EventDown + +var CreateNetTUN = netstack.CreateNetTUN diff --git a/client/iface/wgbackend/tun_linux.go b/client/iface/wgbackend/tun_linux.go new file mode 100644 index 000000000..0b3cbf36f --- /dev/null +++ b/client/iface/wgbackend/tun_linux.go @@ -0,0 +1,5 @@ +package wgbackend + +import "github.com/netbirdio/wireguard-go/tun" + +var CreateUnmonitoredTUNFromFD = tun.CreateUnmonitoredTUNFromFD diff --git a/client/iface/wgbackend/tun_unix.go b/client/iface/wgbackend/tun_unix.go new file mode 100644 index 000000000..12b39c36d --- /dev/null +++ b/client/iface/wgbackend/tun_unix.go @@ -0,0 +1,10 @@ +//go:build linux || darwin || freebsd || openbsd + +package wgbackend + +import "github.com/netbirdio/wireguard-go/tun" + +var ( + CreateTUN = tun.CreateTUN + CreateTUNFromFile = tun.CreateTUNFromFile +) diff --git a/client/iface/wgbackend/tun_windows.go b/client/iface/wgbackend/tun_windows.go new file mode 100644 index 000000000..7d6a8ceac --- /dev/null +++ b/client/iface/wgbackend/tun_windows.go @@ -0,0 +1,7 @@ +package wgbackend + +import "github.com/netbirdio/wireguard-go/tun" + +type NativeTUN = tun.NativeTun + +var CreateTUNWithRequestedGUID = tun.CreateTUNWithRequestedGUID diff --git a/client/installer.nsis b/client/installer.nsis index 304d38864..732932ac1 100644 --- a/client/installer.nsis +++ b/client/installer.nsis @@ -198,6 +198,15 @@ FunctionEnd ###################################################################### Section -MainProgram ${INSTALL_TYPE} + ; The previous uninstaller stops the service and the UI only when its uninstall key + ; is found and endpoint protection lets the unsigned uninstaller run. Extraction + ; fails on any exe that is still running. + DetailPrint "Stopping Netbird service..." + ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop' + DetailPrint "Terminating ${UI_APP_NAME} process..." + ExecWait '"$SYSDIR\taskkill.exe" /im ${UI_APP_EXE}.exe /f' + ; Both return before the processes release their image files. + Sleep 3000 # SetOverwrite ifnewer SetOutPath "$INSTDIR" !ifndef ARCH diff --git a/client/internal/acl/mocks/iface_mapper.go b/client/internal/acl/mocks/iface_mapper.go index f8cca1c2d..b4cff0c59 100644 --- a/client/internal/acl/mocks/iface_mapper.go +++ b/client/internal/acl/mocks/iface_mapper.go @@ -8,10 +8,10 @@ import ( reflect "reflect" gomock "go.uber.org/mock/gomock" - wgdevice "golang.zx2c4.com/wireguard/device" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // MockIFaceMapper is a mock of IFaceMapper interface. @@ -108,10 +108,10 @@ func (mr *MockIFaceMapperMockRecorder) GetDevice() *gomock.Call { } // GetWGDevice mocks base method. -func (m *MockIFaceMapper) GetWGDevice() *wgdevice.Device { +func (m *MockIFaceMapper) GetWGDevice() *wgbackend.Device { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetWGDevice") - ret0, _ := ret[0].(*wgdevice.Device) + ret0, _ := ret[0].(*wgbackend.Device) return ret0 } diff --git a/client/internal/dns/response_writer.go b/client/internal/dns/response_writer.go index 287cf28b0..39ea2cd5e 100644 --- a/client/internal/dns/response_writer.go +++ b/client/internal/dns/response_writer.go @@ -7,14 +7,15 @@ import ( "github.com/google/gopacket" "github.com/google/gopacket/layers" "github.com/miekg/dns" - "golang.zx2c4.com/wireguard/tun" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type responseWriter struct { local net.Addr remote net.Addr packet gopacket.Packet - device tun.Device + device wgbackend.TUN } // LocalAddr returns the net.Addr of the server diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 777594272..59d5c0124 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/firewall/uspfilter" @@ -23,6 +22,7 @@ import ( "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/dns/local" "github.com/netbirdio/netbird/client/internal/dns/test" "github.com/netbirdio/netbird/client/internal/dns/types" @@ -83,7 +83,7 @@ func (w *mocWGIface) GetStats(_ string) (configurer.WGStats, error) { return configurer.WGStats{}, nil } -func (w *mocWGIface) GetNet() *netstack.Net { +func (w *mocWGIface) GetNet() *wgbackend.NetstackNet { return nil } diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go index e8c036076..938a6d489 100644 --- a/client/internal/dns/service_memory.go +++ b/client/internal/dns/service_memory.go @@ -108,7 +108,7 @@ func (s *ServiceViaMemory) filterDNSTraffic() error { if s.tcpDNS == nil { if dev := s.wgInterface.GetDevice(); dev != nil { // MTU only affects TCP segment sizing; DNS messages are small so this has no practical impact. - s.tcpDNS = newTCPDNSServer(s.dnsMux, dev.Device, s.runtimeIP, uint16(s.runtimePort), iface.DefaultMTU) + s.tcpDNS = newTCPDNSServer(s.dnsMux, dev.TUN, s.runtimeIP, uint16(s.runtimePort), iface.DefaultMTU) } } @@ -143,7 +143,7 @@ func (s *ServiceViaMemory) filterDNSTraffic() error { writer := &responseWriter{ remote: remoteAddrFromPacket(packet), packet: packet, - device: dev.Device, + device: dev.TUN, } go s.dnsMux.ServeDNS(writer, msg) return true diff --git a/client/internal/dns/tcpstack.go b/client/internal/dns/tcpstack.go index 88e72e767..9419b53dc 100644 --- a/client/internal/dns/tcpstack.go +++ b/client/internal/dns/tcpstack.go @@ -12,7 +12,6 @@ import ( "github.com/miekg/dns" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun" "gvisor.dev/gvisor/pkg/buffer" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" @@ -21,6 +20,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "gvisor.dev/gvisor/pkg/waiter" + + "github.com/netbirdio/netbird/client/iface/wgbackend" ) const ( @@ -38,7 +39,7 @@ type tcpDNSServer struct { s *stack.Stack ep *dnsEndpoint mux *dns.ServeMux - tunDev tun.Device + tunDev wgbackend.TUN ip netip.Addr port uint16 mtu uint16 @@ -49,7 +50,7 @@ type tcpDNSServer struct { timer *time.Timer } -func newTCPDNSServer(mux *dns.ServeMux, tunDev tun.Device, ip netip.Addr, port uint16, mtu uint16) *tcpDNSServer { +func newTCPDNSServer(mux *dns.ServeMux, tunDev wgbackend.TUN, ip netip.Addr, port uint16, mtu uint16) *tcpDNSServer { return &tcpDNSServer{ mux: mux, tunDev: tunDev, @@ -270,7 +271,7 @@ func (t *tcpDNSServer) handleTCPDNS(r *tcp.ForwarderRequest) { // dnsEndpoint implements stack.LinkEndpoint for writing packets back via the tun device. type dnsEndpoint struct { dispatcher stack.NetworkDispatcher - tunDev tun.Device + tunDev wgbackend.TUN mtu atomic.Uint32 } diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index 72fc0450c..863962f9b 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -44,10 +44,10 @@ import ( "github.com/miekg/dns" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/dns/resutil" "github.com/netbirdio/netbird/client/internal/dns/types" "github.com/netbirdio/netbird/client/internal/peer" @@ -714,7 +714,7 @@ func toTCPClient(c *dns.Client) *dns.Client { // ExchangeWithNetstack performs a DNS exchange using netstack for dialing. // This is needed when netstack is enabled to reach peer IPs through the tunnel. -func ExchangeWithNetstack(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upstream string) (*dns.Msg, error) { +func ExchangeWithNetstack(ctx context.Context, nsNet *wgbackend.NetstackNet, r *dns.Msg, upstream string) (*dns.Msg, error) { // If request came in over TCP, go straight to TCP upstream if dnsProtocolFromContext(ctx) == protoTCP { rm, err := netstackExchange(ctx, nsNet, r, upstream, protoTCP) @@ -758,7 +758,7 @@ func ExchangeWithNetstack(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, return reply, nil } -func netstackExchange(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upstream, network string) (*dns.Msg, error) { +func netstackExchange(ctx context.Context, nsNet *wgbackend.NetstackNet, r *dns.Msg, upstream, network string) (*dns.Msg, error) { conn, err := nsNet.DialContext(ctx, network, upstream) if err != nil { return nil, fmt.Errorf("with %s: %w", network, err) diff --git a/client/internal/dns/upstream_general.go b/client/internal/dns/upstream_general.go index dc841757b..b726d431b 100644 --- a/client/internal/dns/upstream_general.go +++ b/client/internal/dns/upstream_general.go @@ -9,15 +9,15 @@ import ( "time" "github.com/miekg/dns" - "golang.zx2c4.com/wireguard/tun/netstack" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/shared/management/domain" ) type upstreamResolver struct { *upstreamResolverBase - nsNet *netstack.Net + nsNet *wgbackend.NetstackNet } func newUpstreamResolver( diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go index 4c2784545..312c1f226 100644 --- a/client/internal/dns/upstream_test.go +++ b/client/internal/dns/upstream_test.go @@ -13,10 +13,10 @@ import ( "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/dns/test" ) @@ -128,7 +128,7 @@ func (m *mockNetstackProvider) ToInterface() *net.Interface { return nil } func (m *mockNetstackProvider) IsUserspaceBind() bool { return false } func (m *mockNetstackProvider) GetFilter() device.PacketFilter { return nil } func (m *mockNetstackProvider) GetDevice() *device.FilteredDevice { return nil } -func (m *mockNetstackProvider) GetNet() *netstack.Net { return nil } +func (m *mockNetstackProvider) GetNet() *wgbackend.NetstackNet { return nil } func (m *mockNetstackProvider) GetInterfaceGUIDString() (string, error) { return "", nil } diff --git a/client/internal/dns/wgiface.go b/client/internal/dns/wgiface.go index 717e16325..9b57bd0c3 100644 --- a/client/internal/dns/wgiface.go +++ b/client/internal/dns/wgiface.go @@ -5,10 +5,9 @@ package dns import ( "net" - "golang.zx2c4.com/wireguard/tun/netstack" - "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // WGIface defines subset methods of interface required for manager @@ -19,5 +18,5 @@ type WGIface interface { IsUserspaceBind() bool GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetNet() *netstack.Net + GetNet() *wgbackend.NetstackNet } diff --git a/client/internal/dns/wgiface_windows.go b/client/internal/dns/wgiface_windows.go index 347e0233a..11d24ead0 100644 --- a/client/internal/dns/wgiface_windows.go +++ b/client/internal/dns/wgiface_windows.go @@ -1,10 +1,9 @@ package dns import ( - "golang.zx2c4.com/wireguard/tun/netstack" - "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) // WGIface defines subset methods of interface required for manager @@ -14,6 +13,6 @@ type WGIface interface { IsUserspaceBind() bool GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetNet() *netstack.Net + GetNet() *wgbackend.NetstackNet GetInterfaceGUIDString() (string, error) } diff --git a/client/internal/dnsfwd/forwarder.go b/client/internal/dnsfwd/forwarder.go index e3cb597be..9d58384ad 100644 --- a/client/internal/dnsfwd/forwarder.go +++ b/client/internal/dnsfwd/forwarder.go @@ -14,10 +14,10 @@ import ( "github.com/hashicorp/go-multierror" "github.com/miekg/dns" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun/netstack" nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/dns/resutil" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/route" @@ -90,7 +90,7 @@ func NewDNSForwarder(listenAddress netip.AddrPort, ttl uint32, firewall firewall } func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error { - var netstackNet *netstack.Net + var netstackNet *wgbackend.NetstackNet if f.wgIface != nil { netstackNet = f.wgIface.GetNet() } @@ -153,7 +153,7 @@ func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error { return <-errCh } -func (f *DNSForwarder) createUDPListener(netstackNet *netstack.Net) (net.PacketConn, error) { +func (f *DNSForwarder) createUDPListener(netstackNet *wgbackend.NetstackNet) (net.PacketConn, error) { if netstackNet != nil { return netstackNet.ListenUDPAddrPort(f.listenAddress) } @@ -161,7 +161,7 @@ func (f *DNSForwarder) createUDPListener(netstackNet *netstack.Net) (net.PacketC return net.ListenUDP("udp", net.UDPAddrFromAddrPort(f.listenAddress)) } -func (f *DNSForwarder) createTCPListener(netstackNet *netstack.Net) (net.Listener, error) { +func (f *DNSForwarder) createTCPListener(netstackNet *wgbackend.NetstackNet) (net.Listener, error) { if netstackNet != nil { return netstackNet.ListenTCPAddrPort(f.listenAddress) } diff --git a/client/internal/dnsfwd/manager.go b/client/internal/dnsfwd/manager.go index 1c62e908d..474555550 100644 --- a/client/internal/dnsfwd/manager.go +++ b/client/internal/dnsfwd/manager.go @@ -9,11 +9,11 @@ import ( "github.com/hashicorp/go-multierror" log "github.com/sirupsen/logrus" - "golang.zx2c4.com/wireguard/tun/netstack" nberrors "github.com/netbirdio/netbird/client/errors" firewall "github.com/netbirdio/netbird/client/firewall/manager" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" "github.com/netbirdio/netbird/client/internal/peer" nbdns "github.com/netbirdio/netbird/dns" @@ -28,7 +28,7 @@ const ( // wgIface defines the interface for WireGuard interface operations needed by the DNS forwarder. type wgIface interface { - GetNet() *netstack.Net + GetNet() *wgbackend.NetstackNet Address() wgaddr.Address } diff --git a/client/internal/engine.go b/client/internal/engine.go index c4bdf5121..4de0209b0 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -18,11 +18,9 @@ import ( "time" "github.com/hashicorp/go-multierror" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" "github.com/pion/stun/v3" log "github.com/sirupsen/logrus" - wgdevice "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/anonymize" @@ -36,6 +34,7 @@ import ( nbnetstack "github.com/netbirdio/netbird/client/iface/netstack" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/acl" "github.com/netbirdio/netbird/client/internal/certproof" "github.com/netbirdio/netbird/client/internal/debug" @@ -245,7 +244,7 @@ type Engine struct { // wgInterface. Reaching the device through wgInterface requires // syncMsgMux, which handleSync holds while it adds and removes peers; // SetPerformance must stay reachable exactly when that work is stuck. - wgDevice atomic.Pointer[wgdevice.Device] + wgDevice atomic.Pointer[wgbackend.Device] udpMux *udpmux.UniversalUDPMuxDefault @@ -2703,7 +2702,7 @@ func (e *Engine) stopDNSForwarder() { e.dnsForwardMgr = nil } -func (e *Engine) GetNet() (*netstack.Net, error) { +func (e *Engine) GetNet() (*wgbackend.NetstackNet, error) { e.syncMsgMux.Lock() intf := e.wgInterface e.syncMsgMux.Unlock() diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 4974eca87..14fcbe5ea 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -14,10 +14,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - wgdevice "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/iface" @@ -61,11 +60,11 @@ type MockWGIface struct { SetFilterFunc func(filter device.PacketFilter) error GetFilterFunc func() device.PacketFilter GetDeviceFunc func() *device.FilteredDevice - GetWGDeviceFunc func() *wgdevice.Device + GetWGDeviceFunc func() *wgbackend.Device GetStatsFunc func() (map[string]configurer.WGStats, error) GetInterfaceGUIDStringFunc func() (string, error) GetProxyFunc func() wgproxy.Proxy - GetNetFunc func() *netstack.Net + GetNetFunc func() *wgbackend.NetstackNet LastActivitiesFunc func() map[string]monotime.Time } @@ -149,7 +148,7 @@ func (m *MockWGIface) GetDevice() *device.FilteredDevice { return m.GetDeviceFunc() } -func (m *MockWGIface) GetWGDevice() *wgdevice.Device { +func (m *MockWGIface) GetWGDevice() *wgbackend.Device { return m.GetWGDeviceFunc() } @@ -161,7 +160,7 @@ func (m *MockWGIface) GetProxy() wgproxy.Proxy { return m.GetProxyFunc() } -func (m *MockWGIface) GetNet() *netstack.Net { +func (m *MockWGIface) GetNet() *wgbackend.NetstackNet { return m.GetNetFunc() } diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go index d772a3a03..69d43a237 100644 --- a/client/internal/iface_common.go +++ b/client/internal/iface_common.go @@ -5,14 +5,13 @@ import ( "net/netip" "time" - wgdevice "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/udpmux" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/monotime" ) @@ -37,9 +36,9 @@ type wgIfaceBase interface { SetFilter(filter device.PacketFilter) error GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetWGDevice() *wgdevice.Device + GetWGDevice() *wgbackend.Device GetStats() (map[string]configurer.WGStats, error) - GetNet() *netstack.Net + GetNet() *wgbackend.NetstackNet FullStats() (*configurer.Stats, error) LastActivities() map[string]monotime.Time SetPresharedKey(peerKey string, psk wgtypes.Key, updateOnly bool) error diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index b8a32b0cb..b3d3a95ae 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" diff --git a/client/internal/peer/guard/ice_monitor.go b/client/internal/peer/guard/ice_monitor.go index a201dd095..ac6290c10 100644 --- a/client/internal/peer/guard/ice_monitor.go +++ b/client/internal/peer/guard/ice_monitor.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" log "github.com/sirupsen/logrus" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" diff --git a/client/internal/peer/ice/agent.go b/client/internal/peer/ice/agent.go index a919cded5..54cafd0c8 100644 --- a/client/internal/peer/ice/agent.go +++ b/client/internal/peer/ice/agent.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" "github.com/pion/logging" "github.com/pion/randutil" log "github.com/sirupsen/logrus" diff --git a/client/internal/peer/ice/config.go b/client/internal/peer/ice/config.go index 9d6b19925..d6047e6dc 100644 --- a/client/internal/peer/ice/config.go +++ b/client/internal/peer/ice/config.go @@ -1,7 +1,7 @@ package ice import ( - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" "github.com/netbirdio/netbird/client/internal/stdnet" ) diff --git a/client/internal/peer/signaler.go b/client/internal/peer/signaler.go index 5e437d96b..590bf3fed 100644 --- a/client/internal/peer/signaler.go +++ b/client/internal/peer/signaler.go @@ -1,7 +1,7 @@ package peer import ( - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go index 257164bbd..03ae19819 100644 --- a/client/internal/peer/worker_ice.go +++ b/client/internal/peer/worker_ice.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/client/iface" diff --git a/client/internal/peer/worker_ice_test.go b/client/internal/peer/worker_ice_test.go index 3089841c2..17f611c7e 100644 --- a/client/internal/peer/worker_ice_test.go +++ b/client/internal/peer/worker_ice_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/pion/ice/v4" + "github.com/netbirdio/ice/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/client/internal/routemanager/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go index 9b7bce751..8e8a507bf 100644 --- a/client/internal/routemanager/iface/iface_common.go +++ b/client/internal/routemanager/iface/iface_common.go @@ -4,10 +4,9 @@ import ( "net" "net/netip" - "golang.zx2c4.com/wireguard/tun/netstack" - "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" ) type wgIfaceBase interface { @@ -20,5 +19,5 @@ type wgIfaceBase interface { IsUserspaceBind() bool GetFilter() device.PacketFilter GetDevice() *device.FilteredDevice - GetNet() *netstack.Net + GetNet() *wgbackend.NetstackNet } diff --git a/client/internal/routemanager/reconcile_test.go b/client/internal/routemanager/reconcile_test.go index bc9693229..7f409c560 100644 --- a/client/internal/routemanager/reconcile_test.go +++ b/client/internal/routemanager/reconcile_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" "github.com/netbirdio/netbird/client/internal/routemanager/refcounter" ) @@ -48,7 +48,7 @@ func (m *reconcileWGMock) ToInterface() *net.Interface { return n func (m *reconcileWGMock) IsUserspaceBind() bool { return false } func (m *reconcileWGMock) GetFilter() device.PacketFilter { return nil } func (m *reconcileWGMock) GetDevice() *device.FilteredDevice { return nil } -func (m *reconcileWGMock) GetNet() *netstack.Net { return nil } +func (m *reconcileWGMock) GetNet() *wgbackend.NetstackNet { return nil } // TestReconcilePeerAllowedIPs verifies the declarative reconcile re-applies every routed prefix // tracked for the peer (self-heal, independent of refcount level) and stays scoped to that peer. diff --git a/client/ssh/server/server.go b/client/ssh/server/server.go index b32da796e..a33784329 100644 --- a/client/ssh/server/server.go +++ b/client/ssh/server/server.go @@ -20,9 +20,9 @@ import ( log "github.com/sirupsen/logrus" cryptossh "golang.org/x/crypto/ssh" "golang.org/x/exp/maps" - "golang.zx2c4.com/wireguard/tun/netstack" "github.com/netbirdio/netbird/client/iface/wgaddr" + "github.com/netbirdio/netbird/client/iface/wgbackend" sshauth "github.com/netbirdio/netbird/client/ssh/auth" "github.com/netbirdio/netbird/client/ssh/detection" "github.com/netbirdio/netbird/shared/auth" @@ -167,7 +167,7 @@ type Server struct { allowSFTP bool jwtEnabled bool - netstackNet *netstack.Net + netstackNet *wgbackend.NetstackNet wgAddress wgaddr.Address @@ -445,7 +445,7 @@ func (s *Server) buildSessionInfo(state *sessionState) SessionInfo { } // SetNetstackNet sets the netstack network for userspace networking -func (s *Server) SetNetstackNet(net *netstack.Net) { +func (s *Server) SetNetstackNet(net *wgbackend.NetstackNet) { s.mu.Lock() defer s.mu.Unlock() s.netstackNet = net diff --git a/client/ui/frontend/.prettierrc b/client/ui/frontend/.prettierrc index e47a94f56..be99d3270 100644 --- a/client/ui/frontend/.prettierrc +++ b/client/ui/frontend/.prettierrc @@ -8,5 +8,6 @@ "arrowParens": "always", "endOfLine": "lf", "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./src/globals.css", "tailwindFunctions": ["cn", "clsx", "cva", "tw"] } diff --git a/client/ui/frontend/package.json b/client/ui/frontend/package.json index dcef99ad3..e8b58fde0 100644 --- a/client/ui/frontend/package.json +++ b/client/ui/frontend/package.json @@ -46,11 +46,11 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.3", "@types/node": "^25.6.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", "eslint": "^9.39.4", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", @@ -59,9 +59,9 @@ "globals": "^17.6.0", "postcss": "^8.5.1", "prettier": "^3.8.3", - "prettier-plugin-tailwindcss": "^0.8.0", - "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7", + "prettier-plugin-tailwindcss": "^0.8.1", + "tailwindcss": "^4.3.3", + "tw-animate-css": "^1.4.0", "typescript": "^5.7.3", "typescript-eslint": "^8.61.1", "vite": "^6.0.7" diff --git a/client/ui/frontend/pnpm-lock.yaml b/client/ui/frontend/pnpm-lock.yaml index b6b3dd336..3d05e8064 100644 --- a/client/ui/frontend/pnpm-lock.yaml +++ b/client/ui/frontend/pnpm-lock.yaml @@ -10,37 +10,37 @@ importers: dependencies: '@radix-ui/react-dialog': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 - version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.8 - version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.1.15 - version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.3.8 - version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.5.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.2.10 - version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-switch': specifier: ^1.2.6 - version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.4.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': specifier: ^1.1.13 - version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.2.8 - version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.4.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-visually-hidden': specifier: ^1.2.4 - version: 1.2.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wailsio/runtime': specifier: latest - version: 3.0.0-alpha.79 + version: 3.0.0-beta.33 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -49,13 +49,13 @@ importers: version: 2.1.1 cmdk: specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) framer-motion: specifier: ^12.38.0 - version: 12.40.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 12.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) i18next: specifier: ^26.2.0 - version: 26.3.0(typescript@5.9.3) + version: 26.5.3(typescript@5.9.3) lucide-react: specifier: ^0.566.0 version: 0.566.0(react@18.3.1) @@ -67,87 +67,83 @@ importers: version: 18.3.1(react@18.3.1) react-i18next: specifier: ^17.0.8 - version: 17.0.8(i18next@26.3.0(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 17.1.0(i18next@26.5.3(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-loading-skeleton: specifier: ^3.5.0 version: 3.5.0(react@18.3.1) react-router-dom: specifier: ^7.1.3 - version: 7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-virtuoso: specifier: ^4.12.5 - version: 4.18.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.18.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.6.0 version: 2.6.1 devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@9.39.4(jiti@1.21.7)) + version: 10.0.1(eslint@9.39.5(jiti@2.7.0)) + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0)) '@types/node': specifier: ^25.6.0 - version: 25.9.1 + version: 25.9.9 '@types/react': specifier: ^18.3.18 - version: 18.3.29 + version: 18.3.31 '@types/react-dom': specifier: ^18.3.5 - version: 18.3.7(@types/react@18.3.29) + version: 18.3.7(@types/react@18.3.31) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7)) - autoprefixer: - specifier: ^10.4.20 - version: 10.5.0(postcss@8.5.15) + version: 4.7.0(vite@6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0)) eslint: specifier: ^9.39.4 - version: 9.39.4(jiti@1.21.7) + version: 9.39.5(jiti@2.7.0) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.39.4(jiti@1.21.7)) + version: 6.10.2(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@1.21.7)) + version: 7.37.5(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@9.39.4(jiti@1.21.7)) + version: 7.1.1(eslint@9.39.5(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.5.3 - version: 0.5.3(eslint@9.39.4(jiti@1.21.7)) + version: 0.5.7(eslint@9.39.5(jiti@2.7.0)) globals: specifier: ^17.6.0 - version: 17.6.0 + version: 17.13.0 postcss: specifier: ^8.5.1 - version: 8.5.15 + version: 8.5.31 prettier: specifier: ^3.8.3 - version: 3.8.3 + version: 3.9.10 prettier-plugin-tailwindcss: - specifier: ^0.8.0 - version: 0.8.0(prettier@3.8.3) + specifier: ^0.8.1 + version: 0.8.1(prettier@3.9.10) tailwindcss: - specifier: ^3.4.17 - version: 3.4.19 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.19) + specifier: ^4.3.3 + version: 4.3.3 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ^5.7.3 version: 5.9.3 typescript-eslint: specifier: ^8.61.1 - version: 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + version: 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) vite: specifier: ^6.0.7 - version: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + version: 6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0) packages: - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -160,8 +156,8 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.29.7': @@ -198,12 +194,12 @@ packages: resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.7': - resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true @@ -219,20 +215,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} engines: {node: '>=6.9.0'} '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} '@esbuild/aix-ppc64@0.25.12': @@ -391,8 +387,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -413,8 +409,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@10.0.1': @@ -426,8 +422,8 @@ packages: eslint: optional: true - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -438,20 +434,20 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/react-dom@2.1.8': - resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + '@floating-ui/react-dom@2.1.9': + resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -483,32 +479,27 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@radix-ui/number@1.1.3': + resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@radix-ui/primitive@1.2.0': + resolution: {integrity: sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==} - '@radix-ui/number@1.1.1': - resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} - - '@radix-ui/primitive@1.1.3': - resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} - - '@radix-ui/react-arrow@1.1.7': - resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + '@radix-ui/react-arrow@1.1.17': + resolution: {integrity: sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -520,8 +511,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-collection@1.1.7': - resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + '@radix-ui/react-collection@1.2.0': + resolution: {integrity: sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -533,8 +524,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-compose-refs@1.1.2': - resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + '@radix-ui/react-compose-refs@1.1.5': + resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -542,8 +533,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-context@1.1.2': - resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + '@radix-ui/react-context@1.3.0': + resolution: {integrity: sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -551,8 +542,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.15': - resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + '@radix-ui/react-dialog@1.3.1': + resolution: {integrity: sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -564,8 +555,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-direction@1.1.1': - resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + '@radix-ui/react-direction@1.2.0': + resolution: {integrity: sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -573,8 +564,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.11': - resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + '@radix-ui/react-dismissable-layer@1.2.1': + resolution: {integrity: sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -586,8 +577,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.16': - resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + '@radix-ui/react-dropdown-menu@2.2.1': + resolution: {integrity: sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -599,8 +590,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-guards@1.1.3': - resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + '@radix-ui/react-focus-guards@1.2.0': + resolution: {integrity: sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -608,8 +599,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-focus-scope@1.1.7': - resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + '@radix-ui/react-focus-scope@1.3.1': + resolution: {integrity: sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -621,8 +612,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-id@1.1.1': - resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + '@radix-ui/react-id@1.1.4': + resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} peerDependencies: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc @@ -630,8 +621,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-label@2.1.8': - resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + '@radix-ui/react-label@2.1.17': + resolution: {integrity: sha512-xmj7MN/drx1oUAXkfiFIvyDmGNUIN/omGobvA5oAoNqu9zO2MkISYGZOkBcqSzs/UZXXwV+/3ACRE7q7mGcmkA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -643,8 +634,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-menu@2.1.16': - resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + '@radix-ui/react-menu@2.2.1': + resolution: {integrity: sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -656,8 +647,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popover@1.1.15': - resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + '@radix-ui/react-popover@1.3.1': + resolution: {integrity: sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -669,8 +660,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-popper@1.2.8': - resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + '@radix-ui/react-popper@1.4.0': + resolution: {integrity: sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -682,8 +673,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + '@radix-ui/react-portal@1.2.0': + resolution: {integrity: sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -695,8 +686,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + '@radix-ui/react-presence@1.1.11': + resolution: {integrity: sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -708,8 +699,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + '@radix-ui/react-primitive@2.2.0': + resolution: {integrity: sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -721,8 +712,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.4': - resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + '@radix-ui/react-radio-group@1.5.1': + resolution: {integrity: sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -734,8 +725,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-radio-group@1.3.8': - resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + '@radix-ui/react-roving-focus@1.2.1': + resolution: {integrity: sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -747,8 +738,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-roving-focus@1.1.11': - resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + '@radix-ui/react-scroll-area@1.4.0': + resolution: {integrity: sha512-CFrLqHNejWq5FhKAcj9V/WPGRsol76HBQto81NdOBVFEOqIEkJkKIJt0Z824A/9hHLCtoiR/Kw3euRgzXP2pxg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -760,8 +751,17 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-scroll-area@1.2.10': - resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + '@radix-ui/react-slot@1.5.0': + resolution: {integrity: sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.4.1': + resolution: {integrity: sha512-NpBGwc40rY02ZmSvuyeKk0Bvc8ZsNeTMi4qHvJ+ZH4EiYCv0zIXEgdCW4bVdzgIUeP9xxbLJSxKOUfqITYNmkA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -773,26 +773,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.2.4': - resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-switch@1.2.6': - resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + '@radix-ui/react-tabs@1.2.1': + resolution: {integrity: sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -804,8 +786,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tabs@1.1.13': - resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + '@radix-ui/react-tooltip@1.4.1': + resolution: {integrity: sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -817,8 +799,71 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tooltip@1.2.8': - resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + '@radix-ui/react-use-callback-ref@1.1.5': + resolution: {integrity: sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.3.0': + resolution: {integrity: sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.5': + resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.3': + resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.4': + resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.4': + resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.5': + resolution: {integrity: sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.13': + resolution: {integrity: sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -830,248 +875,244 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-previous@1.1.1': - resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-rect@1.1.1': - resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-size@1.1.1': - resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-visually-hidden@1.2.3': - resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-visually-hidden@1.2.4': - resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/rect@1.1.1': - resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@radix-ui/rect@1.1.3': + resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + '@rollup/rollup-android-arm-eabi@4.64.12': + resolution: {integrity: sha512-Z8lz+5UOiCJLonjeqwJlhUtHA/Sr6R1AidkccQxdWF0nigNzP81aLoU+udgOaGfL5s1uzFXzcux/pCwGAMu0lw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + '@rollup/rollup-android-arm64@4.64.12': + resolution: {integrity: sha512-+tKxNa2psFmiTb0KfXlXs4Yj7vkEhzAvZ0DrQ5+0gWHQJM10jSleQ3l1FIVm5uvD53YM8lHfM4mgCLDDN9f1Gg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + '@rollup/rollup-darwin-arm64@4.64.12': + resolution: {integrity: sha512-iiripdfvyRBugV55/Dk7GplNXQtN/PG85OcKeveU1z2UkTg0MofbBF1qk//qgPWiqJbVvQcXAVCfE56oDGO4EQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + '@rollup/rollup-darwin-x64@4.64.12': + resolution: {integrity: sha512-BmATib8swSMAagWNIfDZMnb+k34RXR4bbGOUfZgK8lLlniuMyVoSDDXCOGkdhtUmBXL4aygDh7bhjQ+TmhTvIw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + '@rollup/rollup-freebsd-arm64@4.64.12': + resolution: {integrity: sha512-GzrO0CiUq8vcAI/hYJuIcp6wNm3T3YhOpumYtf9Z3Fz+cfN1VESCVfy7EPcM3/m2K8l+Gn2U+4XtNZjwhUXctQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + '@rollup/rollup-freebsd-x64@4.64.12': + resolution: {integrity: sha512-Dpta6gigm4o6SYioXtTkZBlGLA/Sl6OpzgLqY+51uafJE2blPejAtR1w82CVLPN+F9cvo1+/em92x/zcOgY0uQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + '@rollup/rollup-linux-arm-gnueabihf@4.64.12': + resolution: {integrity: sha512-YpeDwQ5uFaogRvytNC8RK0keDO65kI5wNEhGSs+4GiJc3KHhxj4j5cnQh2nCu9ybJj6RP0/ycB6bkN0yFJ9I+w==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + '@rollup/rollup-linux-arm-musleabihf@4.64.12': + resolution: {integrity: sha512-LQrB3/sImIr1ycrlEyaApOLH1rAy8JV4/dH4eoK817aWATdcs4fESXz53/w72NwDnwPM5zEUpoL7socH4ZDnwA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + '@rollup/rollup-linux-arm64-gnu@4.64.12': + resolution: {integrity: sha512-iAF2iYu9S4SBfNsb4k0ewNBjO83WxHEltIuKQBGMzEKFgrYfmJn+gatBDXR2t/0zJN0UW0kzR4rfvQ1UHzkZ1A==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + '@rollup/rollup-linux-arm64-musl@4.64.12': + resolution: {integrity: sha512-0MQdRtdbirJCXPIVeW/hd/LXLR8Qz96pc+2EBptuuV5q/zSGOzND7SZt8gBTE58NC9cUG/8UxcUXQiKRZ2WzzA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + '@rollup/rollup-linux-loong64-gnu@4.64.12': + resolution: {integrity: sha512-yGDEwn0AGnrY/KCz2oY8R9KKNiQnTcwNuCWXeSXF0VLxXzlqoCgaS5QHsf1lbQvKWwPuN2Czkjk1wXRCBONTOQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + '@rollup/rollup-linux-loong64-musl@4.64.12': + resolution: {integrity: sha512-AjlbD0efZEhHaJ3bsNjxSFDoguNq6VZvC+FKGJGBCA+ZN7BrZq2TuuSWFa6AI8IpOeuvhp2LoYcP7jiZwmtU0Q==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + '@rollup/rollup-linux-ppc64-gnu@4.64.12': + resolution: {integrity: sha512-sDW0CiPoUCEEUV25uBG5sJ2gKe75DJqLpj91m1eK1vKffywyY7v4i6q1450ZuR6QW4NArXg7JpbMGgKU8di3HQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + '@rollup/rollup-linux-ppc64-musl@4.64.12': + resolution: {integrity: sha512-VTQYUhMjl8kv999VuYqScgv7P9cR9KuxI/6bkUsEa3bsswqG0+otFjuLZmJ1U+ffEP4IAz0jxOeV0DX6NRBhjg==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + '@rollup/rollup-linux-riscv64-gnu@4.64.12': + resolution: {integrity: sha512-ppsKwpSjNrzJ8hge3Z3uiCcoRgAAQk2tU7auy3MFF348ffWRldFPnWIT/a/enK7huijlTfnwn4DYpDoapEb45A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + '@rollup/rollup-linux-riscv64-musl@4.64.12': + resolution: {integrity: sha512-Gc3cVRC1trFH+GFWWTsJz6YuD4u75fzr/q99jqQGimonN/zvN6Wecck5WGzllOHj+XYXHcq0Ibor0tanGjfI6A==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + '@rollup/rollup-linux-s390x-gnu@4.64.12': + resolution: {integrity: sha512-pjmDTFStvwFE3shTeUDvSDiSp8/qAf5bQXejcEUDDavWz4jEhTIpCdpS06G0PEwSCSNK0Lngjz2v9l4hk3Z74A==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + '@rollup/rollup-linux-x64-gnu@4.64.12': + resolution: {integrity: sha512-mkbjxPqpR91u1JD7xY4aLZhr/Mqka4/RqGcG4WNGgN/lC1o7TzyCyqJoSJXp9qVVxMwpJGFlEF1VlERb+Ghw1Q==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + '@rollup/rollup-linux-x64-musl@4.64.12': + resolution: {integrity: sha512-vIeut2LkuVc3gcsawLJPahNAzvzBcFRhu2O+yVb//BuQKz7PIItMF0piXRbN/lIDO3d68TSDd368Vg6YwgTybw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + '@rollup/rollup-openbsd-x64@4.64.12': + resolution: {integrity: sha512-3Sx+D3r9oPkTdNUcPrIRM2Am+vzEV6VGWoImHG4ZVK2jJeOW+eDjLeTm6nvu/z/m0fiX2YmGLXIypblc336MXg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + '@rollup/rollup-openharmony-arm64@4.64.12': + resolution: {integrity: sha512-Nn2xh0teFnl71EiiUrKvECWcdGXtWF8q0uGclYwXGVw6QClGAcME0/bJSGdK+xIvd6MLEKYmjkTUvCBV+7m5Kw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + '@rollup/rollup-win32-arm64-msvc@4.64.12': + resolution: {integrity: sha512-n1/0H9TS+kBa4xp6n9aV7yiynBNfmerjkj2nAMIfY0ZHv1zShWRXnsAuAPAH0sfEK4ZSMK+z24q2RjAPFLCN1w==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + '@rollup/rollup-win32-ia32-msvc@4.64.12': + resolution: {integrity: sha512-AdlDbQpsLfGlsHr1NfLZFtdnvxCKNp9TwlYVYExs8JJLWjLafUQEKJk0r9zUbmU57mw9nglMEvzqPHuLWYmaKw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + '@rollup/rollup-win32-x64-gnu@4.64.12': + resolution: {integrity: sha512-u5SSUJBHod51UamjkigSr+JPre6xMy5vaWTTV2K0SnFG4RuKyYpf66dTSLFKX2cwaldpwyBurXPUCxq8riQMEg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + '@rollup/rollup-win32-x64-msvc@4.64.12': + resolution: {integrity: sha512-j5rXD4R/4DM5+V991AA9xOnG2Rwu2g3p5TPgu1Eu7apju0NUrLl+Bn/KpWc6wYJtlXqzwz1r+LNV1JeBU4VGvw==} cpu: [x64] os: [win32] + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1084,14 +1125,14 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.9.1': - resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/node@25.9.9': + resolution: {integrity: sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1101,66 +1142,66 @@ packages: peerDependencies: '@types/react': ^18.0.0 - '@types/react@18.3.29': - resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} - '@typescript-eslint/eslint-plugin@8.61.1': - resolution: {integrity: sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==} + '@typescript-eslint/eslint-plugin@8.72.0': + resolution: {integrity: sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.61.1 + '@typescript-eslint/parser': ^8.72.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.61.1': - resolution: {integrity: sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==} + '@typescript-eslint/parser@8.72.0': + resolution: {integrity: sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.61.1': - resolution: {integrity: sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==} + '@typescript-eslint/project-service@8.72.0': + resolution: {integrity: sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.61.1': - resolution: {integrity: sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==} + '@typescript-eslint/scope-manager@8.72.0': + resolution: {integrity: sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.61.1': - resolution: {integrity: sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==} + '@typescript-eslint/tsconfig-utils@8.72.0': + resolution: {integrity: sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.61.1': - resolution: {integrity: sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==} + '@typescript-eslint/type-utils@8.72.0': + resolution: {integrity: sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.61.1': - resolution: {integrity: sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==} + '@typescript-eslint/types@8.72.0': + resolution: {integrity: sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.61.1': - resolution: {integrity: sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==} + '@typescript-eslint/typescript-estree@8.72.0': + resolution: {integrity: sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.61.1': - resolution: {integrity: sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==} + '@typescript-eslint/utils@8.72.0': + resolution: {integrity: sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.61.1': - resolution: {integrity: sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==} + '@typescript-eslint/visitor-keys@8.72.0': + resolution: {integrity: sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-react@4.7.0': @@ -1169,16 +1210,16 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@wailsio/runtime@3.0.0-alpha.79': - resolution: {integrity: sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==} + '@wailsio/runtime@3.0.0-beta.33': + resolution: {integrity: sha512-3fiyyS06cdiZZk3aLnu+VEY3V8+vFzPZTJMDLIBCJ0Pe8pELn/aVm73fRj7amkYhpEUg24tsuVswnLJAKcvfig==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} engines: {node: '>=0.4.0'} hasBin: true @@ -1189,16 +1230,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1214,8 +1245,8 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} engines: {node: '>= 0.4'} array.prototype.findlast@1.2.5: @@ -1245,19 +1276,12 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - autoprefixer@10.5.0: - resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.12.1: - resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + axe-core@4.14.0: + resolution: {integrity: sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -1271,28 +1295,20 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.10.32: - resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + baseline-browser-mapping@2.11.30: + resolution: {integrity: sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==} engines: {node: '>=6.0.0'} hasBin: true - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@1.1.15: - resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.29.4: + resolution: {integrity: sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1312,21 +1328,13 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + caniuse-lite@1.0.30001817: + resolution: {integrity: sha512-uZ1JG80BwhtMalIAGPxPfmNhHyd/B/nTdr3hITWlRamSyAdUStP1Gop01e7SD5q6J90ixJvWDbvmiFVfDgn4bw==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1347,10 +1355,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1365,11 +1369,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1408,15 +1407,13 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1425,12 +1422,16 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.362: - resolution: {integrity: sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==} + electron-to-chromium@1.5.460: + resolution: {integrity: sha512-rRaVC7bNUyGsL+GMtmZfv3AfgG6za7gE9v5bT0PIbjEerddA0IQ+tCK05NFAXqCpNlrxBZqbFqquyxHRQONHTA==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.26.0: + resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==} + engines: {node: '>=10.13.0'} + es-abstract-get@1.0.0: resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} engines: {node: '>= 0.4'} @@ -1447,8 +1448,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.3.3: - resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} engines: {node: '>= 0.4'} es-object-atoms@1.1.2: @@ -1463,8 +1464,8 @@ packages: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.1: - resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} esbuild@0.25.12: @@ -1492,8 +1493,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - eslint-plugin-react-refresh@0.5.3: - resolution: {integrity: sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==} + eslint-plugin-react-refresh@0.5.7: + resolution: {integrity: sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==} peerDependencies: eslint: ^9 || ^10 @@ -1519,9 +1520,10 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -1552,19 +1554,12 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1578,10 +1573,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1590,18 +1581,15 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - - framer-motion@12.40.0: - resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==} + framer-motion@12.43.0: + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1653,10 +1641,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1665,8 +1649,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@17.6.0: - resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + globals@17.13.0: + resolution: {integrity: sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==} engines: {node: '>=18'} globalthis@1.0.4: @@ -1677,6 +1661,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1700,10 +1687,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} - hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -1714,13 +1697,13 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - html-parse-stringify@3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-parse-stringify@4.0.1: + resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==} - i18next@26.3.0: - resolution: {integrity: sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==} + i18next@26.5.3: + resolution: {integrity: sha512-6pRwOwJ6lBCBB66fqNZaSoUmvsALHMord7FL6GsDhubT1CrPabTMEJIqddm0nn2YEbKofdI/3VAtGrQPuztY1Q==} peerDependencies: - typescript: ^5 || ^6 + typescript: ^5 || ^6 || ^7 peerDependenciesMeta: typescript: optional: true @@ -1729,8 +1712,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.13: + resolution: {integrity: sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -1757,10 +1740,6 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -1769,8 +1748,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -1813,10 +1792,6 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1863,15 +1838,15 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} - jiti@1.21.7: - resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsesc@3.1.0: @@ -1911,12 +1886,79 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lilconfig@3.1.3: - resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} - engines: {node: '>=14'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -1937,27 +1979,22 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - motion-dom@12.40.0: - resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==} + motion-dom@12.43.0: + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} motion-utils@12.39.0: resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} @@ -1965,37 +2002,26 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-exports-info@1.6.0: - resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} - node-releases@2.0.46: - resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + node-releases@2.0.59: + resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} engines: {node: '>=18'} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2024,8 +2050,8 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} engines: {node: '>= 0.4'} p-limit@3.1.0: @@ -2054,79 +2080,24 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.1.0: - resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true - - postcss-nested@6.2.0: - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.31: + resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-tailwindcss@0.8.0: - resolution: {integrity: sha512-V8ITGH87yuBDF6JpEZTOVlUz/saAwqb8f3HRgUj8Lh+tGCcrmorhsLpYqzygwFwK0PE2Ib6Mv3M7T/uE2tZV1g==} + prettier-plugin-tailwindcss@0.8.1: + resolution: {integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==} engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -2180,8 +2151,8 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} engines: {node: '>=14'} hasBin: true @@ -2192,22 +2163,19 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: react: ^18.3.1 - react-i18next@17.0.8: - resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + react-i18next@17.1.0: + resolution: {integrity: sha512-4VouOnTDyvhvqXx6WZ+s3/cgSiaLutILdYkO3P1Ms+4qcKR7At+JP4Vi7IeHkVLyVV+DmN1l23Pn73UcwSGGPA==} peerDependencies: i18next: '>= 26.2.0' react: '>= 16.8.0' react-dom: '*' react-native: '*' - typescript: ^5 || ^6 + typescript: ^5 || ^6 || ^7 peerDependenciesMeta: react-dom: optional: true @@ -2248,15 +2216,15 @@ packages: '@types/react': optional: true - react-router-dom@7.15.1: - resolution: {integrity: sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==} + react-router-dom@7.18.4: + resolution: {integrity: sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.15.1: - resolution: {integrity: sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==} + react-router@7.18.4: + resolution: {integrity: sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -2275,8 +2243,8 @@ packages: '@types/react': optional: true - react-virtuoso@4.18.7: - resolution: {integrity: sha512-xNF5zDGEEIMB7cKwcen/pLig0YDf6OnfFrVgKFa7sHPf9fRem0CaLshyObbBcP88jzn0enavL39EgplgdyT21g==} + react-virtuoso@4.18.19: + resolution: {integrity: sha512-dewyZisA5IiQTldh7eHM5KpI+qxi0nKRVYi2iM01XmPkNa5tjvVUKJKAIc6ted1hAK1/NFlXuhXpVYqGHHdcdQ==} peerDependencies: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' @@ -2285,13 +2253,6 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2304,28 +2265,16 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - resolve@2.0.0-next.7: resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + rollup@4.64.12: + resolution: {integrity: sha512-GhAu8ZglZ61VWSX/ZW3K/qkazgURBIQL0/Yy51QsS8DPyebR5Cf/Z2PANJFpTU0aVZ9byJcyqR6xvBbkXhdGYQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} @@ -2345,8 +2294,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.4: - resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -2389,8 +2338,8 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} stop-iteration-iterator@1.1.0: @@ -2401,8 +2350,8 @@ packages: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} engines: {node: '>= 0.4'} string.prototype.repeat@1.0.0: @@ -2424,11 +2373,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2440,43 +2384,29 @@ packages: tailwind-merge@2.6.1: resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} - tailwindcss-animate@1.0.7: - resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - tailwindcss@3.4.19: - resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} - engines: {node: '>=14.0.0'} - hasBin: true + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2489,16 +2419,16 @@ packages: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} engines: {node: '>= 0.4'} typed-array-length@1.0.8: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.61.1: - resolution: {integrity: sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==} + typescript-eslint@8.72.0: + resolution: {integrity: sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2516,8 +2446,8 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -2545,16 +2475,13 @@ packages: '@types/react': optional: true - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vite@6.4.2: - resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + vite@6.4.4: + resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -2593,10 +2520,6 @@ packages: yaml: optional: true - void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2609,8 +2532,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.22: - resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} engines: {node: '>= 0.4'} which@2.0.2: @@ -2635,13 +2558,11 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} snapshots: - '@alloc/quick-lru@5.2.0': {} - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2653,14 +2574,14 @@ snapshots: '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -2670,10 +2591,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -2682,7 +2603,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.29.4 lru-cache: 5.1.1 semver: 6.3.1 @@ -2690,8 +2611,8 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -2700,7 +2621,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.10 transitivePeerDependencies: - supports-color @@ -2712,14 +2633,14 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - '@babel/helpers@7.29.7': + '@babel/helpers@7.29.10': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@babel/parser@7.29.7': + '@babel/parser@7.29.9': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: @@ -2731,27 +2652,27 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/runtime@7.29.7': {} + '@babel/runtime@7.29.10': {} '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.10': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 @@ -2834,9 +2755,9 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))': dependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2857,7 +2778,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': + '@eslint/eslintrc@3.3.7': dependencies: ajv: 6.15.0 debug: 4.4.3 @@ -2865,17 +2786,17 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.2 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@10.0.1(eslint@9.39.4(jiti@1.21.7))': + '@eslint/js@10.0.1(eslint@9.39.5(jiti@2.7.0))': optionalDependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@2.7.0) - '@eslint/js@9.39.4': {} + '@eslint/js@9.39.5': {} '@eslint/object-schema@2.1.7': {} @@ -2884,22 +2805,22 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.6 + '@floating-ui/dom': 1.8.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} '@humanfs/core@0.19.2': dependencies: @@ -2919,7 +2840,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -2929,656 +2850,690 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - '@nodelib/fs.scandir@2.1.5': + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@radix-ui/number@1.1.3': {} + + '@radix-ui/primitive@1.2.0': {} + + '@radix-ui/react-arrow@1.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@radix-ui/number@1.1.1': {} - - '@radix-ui/primitive@1.1.3': {} - - '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.29)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.5(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - '@radix-ui/react-context@1.1.2(@types/react@18.3.29)(react@18.3.1)': + '@radix-ui/react-context@1.3.0(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-direction@1.1.1(@types/react@18.3.29)(react@18.3.1)': + '@radix-ui/react-direction@1.2.0(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-menu': 2.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.29)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.2.0(@types/react@18.3.31)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-id@1.1.1(@types/react@18.3.29)(react@18.3.1)': + '@radix-ui/react-id@1.1.4(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-label@2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@18.3.31)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.2(@types/react@18.3.29)(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/rect': 1.1.1 + '@floating-ui/react-dom': 2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/rect': 1.1.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-radio-group@1.5.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.4(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-collection': 1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-scroll-area@1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/number': 1.1.3 + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-slot@1.5.0(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/number': 1.1.1 - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-switch@1.4.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.5(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-slot@1.2.3(@types/react@18.3.29)(react@18.3.1)': + '@radix-ui/react-tabs@1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-slot@1.2.4(@types/react@18.3.29)(react@18.3.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-direction': 1.2.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.4.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-context': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.2.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-scope': 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-popper': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.5.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.3.0(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.5(@types/react@18.3.31)(react@18.3.1)': dependencies: - '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-controllable-state@1.3.0(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.2.0 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-effect-event@0.0.5(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@18.3.31)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@18.3.31)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-rect@1.1.4(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.3 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-use-size@1.1.5(@types/react@18.3.31)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.3.31)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.31 + + '@radix-ui/react-visually-hidden@1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.29)(react@18.3.1)': - dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.29)(react@18.3.1)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.29)(react@18.3.1)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.29)(react@18.3.1)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.29)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.29)(react@18.3.1)': - dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.29)(react@18.3.1)': - dependencies: - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.29)(react@18.3.1)': - dependencies: - '@radix-ui/rect': 1.1.1 - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.29)(react@18.3.1)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.29)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.29 - - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) - - '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.29 - '@types/react-dom': 18.3.7(@types/react@18.3.29) - - '@radix-ui/rect@1.1.1': {} + '@radix-ui/rect@1.1.3': {} '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/rollup-android-arm-eabi@4.60.4': + '@rollup/rollup-android-arm-eabi@4.64.12': optional: true - '@rollup/rollup-android-arm64@4.60.4': + '@rollup/rollup-android-arm64@4.64.12': optional: true - '@rollup/rollup-darwin-arm64@4.60.4': + '@rollup/rollup-darwin-arm64@4.64.12': optional: true - '@rollup/rollup-darwin-x64@4.60.4': + '@rollup/rollup-darwin-x64@4.64.12': optional: true - '@rollup/rollup-freebsd-arm64@4.60.4': + '@rollup/rollup-freebsd-arm64@4.64.12': optional: true - '@rollup/rollup-freebsd-x64@4.60.4': + '@rollup/rollup-freebsd-x64@4.64.12': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + '@rollup/rollup-linux-arm-gnueabihf@4.64.12': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.4': + '@rollup/rollup-linux-arm-musleabihf@4.64.12': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.4': + '@rollup/rollup-linux-arm64-gnu@4.64.12': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.4': + '@rollup/rollup-linux-arm64-musl@4.64.12': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.4': + '@rollup/rollup-linux-loong64-gnu@4.64.12': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.4': + '@rollup/rollup-linux-loong64-musl@4.64.12': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.4': + '@rollup/rollup-linux-ppc64-gnu@4.64.12': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.4': + '@rollup/rollup-linux-ppc64-musl@4.64.12': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.4': + '@rollup/rollup-linux-riscv64-gnu@4.64.12': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.4': + '@rollup/rollup-linux-riscv64-musl@4.64.12': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.4': + '@rollup/rollup-linux-s390x-gnu@4.64.12': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.4': + '@rollup/rollup-linux-x64-gnu@4.64.12': optional: true - '@rollup/rollup-linux-x64-musl@4.60.4': + '@rollup/rollup-linux-x64-musl@4.64.12': optional: true - '@rollup/rollup-openbsd-x64@4.60.4': + '@rollup/rollup-openbsd-x64@4.64.12': optional: true - '@rollup/rollup-openharmony-arm64@4.60.4': + '@rollup/rollup-openharmony-arm64@4.64.12': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.4': + '@rollup/rollup-win32-arm64-msvc@4.64.12': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.4': + '@rollup/rollup-win32-ia32-msvc@4.64.12': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.4': + '@rollup/rollup-win32-x64-gnu@4.64.12': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.4': + '@rollup/rollup-win32-x64-msvc@4.64.12': optional: true + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.26.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.2 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0) + '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} - '@types/node@25.9.1': + '@types/node@25.9.9': dependencies: undici-types: 7.24.6 '@types/prop-types@15.7.15': {} - '@types/react-dom@18.3.7(@types/react@18.3.29)': + '@types/react-dom@18.3.7(@types/react@18.3.31)': dependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - '@types/react@18.3.29': + '@types/react@18.3.31': dependencies: '@types/prop-types': 15.7.15 csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.72.0(@typescript-eslint/parser@8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/type-utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.61.1 - eslint: 9.39.4(jiti@1.21.7) - ignore: 7.0.5 + '@typescript-eslint/parser': 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/type-utils': 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.72.0 + eslint: 9.39.5(jiti@2.7.0) + ignore: 7.0.13 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.61.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.72.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) - '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/tsconfig-utils': 8.72.0(typescript@5.9.3) + '@typescript-eslint/types': 8.72.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.61.1': + '@typescript-eslint/scope-manager@8.72.0': dependencies: - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 - '@typescript-eslint/tsconfig-utils@8.61.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.72.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.61.1': {} + '@typescript-eslint/types@8.72.0': {} - '@typescript-eslint/typescript-estree@8.61.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.72.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.61.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.61.1(typescript@5.9.3) - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/visitor-keys': 8.61.1 + '@typescript-eslint/project-service': 8.72.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.72.0(typescript@5.9.3) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.4 - tinyglobby: 0.2.16 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.61.1 - '@typescript-eslint/types': 8.61.1 - '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) - eslint: 9.39.4(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.61.1': + '@typescript-eslint/visitor-keys@8.72.0': dependencies: - '@typescript-eslint/types': 8.61.1 + '@typescript-eslint/types': 8.72.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7))': + '@vitejs/plugin-react@4.7.0(vite@6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -3586,17 +3541,17 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.2(@types/node@25.9.1)(jiti@1.21.7) + vite: 6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0) transitivePeerDependencies: - supports-color - '@wailsio/runtime@3.0.0-alpha.79': {} + '@wailsio/runtime@3.0.0-beta.33': {} - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.19.0): dependencies: - acorn: 8.17.0 + acorn: 8.19.0 - acorn@8.17.0: {} + acorn@8.19.0: {} ajv@6.15.0: dependencies: @@ -3609,15 +3564,6 @@ snapshots: dependencies: color-convert: 2.0.1 - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - arg@5.0.2: {} - argparse@2.0.1: {} aria-hidden@1.2.6: @@ -3631,14 +3577,14 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.9: + array-includes@3.2.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.24.2 es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 + es-shim-unscopables: 1.1.0 is-string: 1.1.1 math-intrinsics: 1.1.0 @@ -3687,20 +3633,11 @@ snapshots: async-function@1.0.0: {} - autoprefixer@10.5.0(postcss@8.5.15): - dependencies: - browserslist: 4.28.2 - caniuse-lite: 1.0.30001793 - fraction.js: 5.3.4 - picocolors: 1.1.1 - postcss: 8.5.15 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.12.1: {} + axe-core@4.14.0: {} axobject-query@4.1.0: {} @@ -3708,30 +3645,24 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.10.32: {} + baseline-browser-mapping@2.11.30: {} - binary-extensions@2.3.0: {} - - brace-expansion@1.1.15: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.6: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 - braces@3.0.3: + browserslist@4.29.4: dependencies: - fill-range: 7.1.1 - - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.32 - caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.362 - node-releases: 2.0.46 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.11.30 + caniuse-lite: 1.0.30001817 + electron-to-chromium: 1.5.460 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.4) call-bind-apply-helpers@1.0.2: dependencies: @@ -3752,39 +3683,25 @@ snapshots: callsites@3.1.0: {} - camelcase-css@2.0.1: {} - - caniuse-lite@1.0.30001793: {} + caniuse-lite@1.0.30001817: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.29)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-dialog': 1.3.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.4(@types/react@18.3.31)(react@18.3.1) + '@radix-ui/react-primitive': 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -3797,8 +3714,6 @@ snapshots: color-name@1.1.4: {} - commander@4.1.1: {} - concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -3811,8 +3726,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - cssesc@3.0.0: {} - csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} @@ -3853,12 +3766,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} - didyoumean@1.2.2: {} - - dlv@1.1.3: {} - doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -3869,10 +3780,15 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.362: {} + electron-to-chromium@1.5.460: {} emoji-regex@9.2.2: {} + enhanced-resolve@5.26.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-abstract-get@1.0.0: dependencies: es-errors: 1.3.0 @@ -3894,7 +3810,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.1 + es-to-primitive: 1.3.4 function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 @@ -3904,7 +3820,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -3920,7 +3836,7 @@ snapshots: object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.7 - own-keys: 1.0.1 + own-keys: 1.0.2 regexp.prototype.flags: 1.5.4 safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 @@ -3932,16 +3848,16 @@ snapshots: string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 + typed-array-byte-offset: 1.0.5 typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-iterator-helpers@1.3.3: + es-iterator-helpers@1.4.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -3969,15 +3885,16 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 - es-to-primitive@1.3.1: + es-to-primitive@1.3.4: dependencies: es-abstract-get: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 @@ -4016,18 +3933,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)): dependencies: aria-query: 5.3.2 - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.12.1 + axe-core: 4.14.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.4(jiti@1.21.7) - hasown: 2.0.3 + eslint: 9.39.5(jiti@2.7.0) + hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.5 @@ -4035,32 +3952,32 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)): dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 - eslint: 9.39.4(jiti@1.21.7) + '@babel/parser': 7.29.9 + eslint: 9.39.5(jiti@2.7.0) hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) + zod: 4.6.7 + zod-validation-error: 4.0.2(zod@4.6.7) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.3(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react-refresh@0.5.7(eslint@9.39.5(jiti@2.7.0)): dependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.5(jiti@2.7.0) - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)): dependencies: - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - es-iterator-helpers: 1.3.3 - eslint: 9.39.4(jiti@1.21.7) + es-iterator-helpers: 1.4.0 + eslint: 9.39.5(jiti@2.7.0) estraverse: 5.3.0 - hasown: 2.0.3 + hasown: 2.0.4 jsx-ast-utils: 3.3.5 minimatch: 3.1.5 object.entries: 1.1.9 @@ -4069,7 +3986,7 @@ snapshots: prop-types: 15.8.1 resolve: 2.0.0-next.7 semver: 6.3.1 - string.prototype.matchall: 4.0.12 + string.prototype.matchall: 4.1.0 string.prototype.repeat: 1.0.0 eslint-scope@8.4.0: @@ -4083,20 +4000,20 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4(jiti@1.21.7): + eslint@9.39.5(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 + '@eslint/eslintrc': 3.3.7 + '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 @@ -4120,14 +4037,14 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.7.0 transitivePeerDependencies: - supports-color espree@10.4.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.19.0 + acorn-jsx: 5.3.2(acorn@8.19.0) eslint-visitor-keys: 4.2.1 esquery@1.7.0: @@ -4144,34 +4061,18 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.7 file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4179,20 +4080,18 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.4: {} for-each@0.3.5: dependencies: is-callable: 1.2.7 - fraction.js@5.3.4: {} - - framer-motion@12.40.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + framer-motion@12.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - motion-dom: 12.40.0 + motion-dom: 12.43.0 motion-utils: 12.39.0 tslib: 2.8.1 optionalDependencies: @@ -4232,7 +4131,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-nonce@1.0.1: {} @@ -4248,17 +4147,13 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 globals@14.0.0: {} - globals@17.6.0: {} + globals@17.13.0: {} globalthis@1.0.4: dependencies: @@ -4267,6 +4162,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -4285,10 +4182,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.3: - dependencies: - function-bind: 1.1.2 - hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -4299,17 +4192,15 @@ snapshots: dependencies: hermes-estree: 0.25.1 - html-parse-stringify@3.0.1: - dependencies: - void-elements: 3.1.0 + html-parse-stringify@4.0.1: {} - i18next@26.3.0(typescript@5.9.3): + i18next@26.5.3(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.13: {} import-fresh@3.3.1: dependencies: @@ -4321,7 +4212,7 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.3 + hasown: 2.0.4 side-channel: 1.1.1 is-array-buffer@3.0.5: @@ -4342,10 +4233,6 @@ snapshots: dependencies: has-bigints: 1.1.0 - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -4353,9 +4240,9 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.16.2: + is-core-module@2.17.0: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: @@ -4399,14 +4286,12 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-number@7.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 is-set@2.0.3: {} @@ -4427,7 +4312,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 is-weakmap@2.0.2: {} @@ -4453,11 +4338,11 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 - jiti@1.21.7: {} + jiti@2.7.0: {} js-tokens@4.0.0: {} - js-yaml@4.2.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -4473,7 +4358,7 @@ snapshots: jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.9 + array-includes: 3.2.0 array.prototype.flat: 1.3.3 object.assign: 4.1.7 object.values: 1.2.1 @@ -4493,9 +4378,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lilconfig@3.1.3: {} + lightningcss-android-arm64@1.32.0: + optional: true - lines-and-columns@1.2.4: {} + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 locate-path@6.0.0: dependencies: @@ -4515,24 +4445,21 @@ snapshots: dependencies: react: 18.3.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + math-intrinsics@1.1.0: {} - merge2@1.4.1: {} - - micromatch@4.0.8: + minimatch@10.2.6: dependencies: - braces: 3.0.3 - picomatch: 2.3.2 - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.12 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.15 + brace-expansion: 1.1.21 - motion-dom@12.40.0: + motion-dom@12.43.0: dependencies: motion-utils: 12.39.0 @@ -4540,31 +4467,21 @@ snapshots: ms@2.1.3: {} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.12: {} + nanoid@3.3.20: {} natural-compare@1.4.0: {} - node-exports-info@1.6.0: + node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 es-errors: 1.3.0 object.entries: 1.1.9 semver: 6.3.1 - node-releases@2.0.46: {} - - normalize-path@3.0.0: {} + node-releases@2.0.59: {} object-assign@4.1.1: {} - object-hash@3.0.0: {} - object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -4608,8 +4525,9 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - own-keys@1.0.1: + own-keys@1.0.2: dependencies: + call-bound: 1.0.4 get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -4634,60 +4552,23 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.2: {} - - picomatch@4.0.4: {} - - pify@2.3.0: {} - - pirates@4.0.7: {} + picomatch@4.0.7: {} possible-typed-array-names@1.1.0: {} - postcss-import@15.1.0(postcss@8.5.15): + postcss@8.5.31: dependencies: - postcss: 8.5.15 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.12 - - postcss-js@4.1.0(postcss@8.5.15): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.5.15 - - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.15 - - postcss-nested@6.2.0(postcss@8.5.15): - dependencies: - postcss: 8.5.15 - postcss-selector-parser: 6.1.2 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 + nanoid: 3.3.20 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prelude-ls@1.2.1: {} - prettier-plugin-tailwindcss@0.8.0(prettier@3.8.3): + prettier-plugin-tailwindcss@0.8.1(prettier@3.9.10): dependencies: - prettier: 3.8.3 + prettier: 3.9.10 - prettier@3.8.3: {} + prettier@3.9.10: {} prop-types@15.8.1: dependencies: @@ -4697,21 +4578,19 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 react: 18.3.1 scheduler: 0.23.2 - react-i18next@17.0.8(i18next@26.3.0(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): + react-i18next@17.1.0(i18next@26.5.3(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): dependencies: - '@babel/runtime': 7.29.7 - html-parse-stringify: 3.0.1 - i18next: 26.3.0(typescript@5.9.3) + '@babel/runtime': 7.29.10 + html-parse-stringify: 4.0.1 + i18next: 26.5.3(typescript@5.9.3) react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) + use-sync-external-store: 1.7.0(react@18.3.1) optionalDependencies: react-dom: 18.3.1(react@18.3.1) typescript: 5.9.3 @@ -4724,32 +4603,32 @@ snapshots: react-refresh@0.17.0: {} - react-remove-scroll-bar@2.3.8(@types/react@18.3.29)(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.31)(react@18.3.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - react-remove-scroll@2.7.2(@types/react@18.3.29)(react@18.3.1): + react-remove-scroll@2.7.2(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.29)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@18.3.29)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.31)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.31)(react@18.3.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.29)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@18.3.29)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.31)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.31)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - react-router-dom@7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router-dom@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router: 7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-router@7.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.1.1 react: 18.3.1 @@ -4757,15 +4636,15 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-style-singleton@2.2.3(@types/react@18.3.29)(react@18.3.1): + react-style-singleton@2.2.3(@types/react@18.3.31)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - react-virtuoso@4.18.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-virtuoso@4.18.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4774,14 +4653,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.9 @@ -4804,59 +4675,47 @@ snapshots: resolve-from@4.0.0: {} - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.2 - node-exports-info: 1.6.0 + is-core-module: 2.17.0 + node-exports-info: 1.6.2 object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} - - rollup@4.60.4: + rollup@4.64.12: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.12 + '@rollup/rollup-android-arm64': 4.64.12 + '@rollup/rollup-darwin-arm64': 4.64.12 + '@rollup/rollup-darwin-x64': 4.64.12 + '@rollup/rollup-freebsd-arm64': 4.64.12 + '@rollup/rollup-freebsd-x64': 4.64.12 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.12 + '@rollup/rollup-linux-arm-musleabihf': 4.64.12 + '@rollup/rollup-linux-arm64-gnu': 4.64.12 + '@rollup/rollup-linux-arm64-musl': 4.64.12 + '@rollup/rollup-linux-loong64-gnu': 4.64.12 + '@rollup/rollup-linux-loong64-musl': 4.64.12 + '@rollup/rollup-linux-ppc64-gnu': 4.64.12 + '@rollup/rollup-linux-ppc64-musl': 4.64.12 + '@rollup/rollup-linux-riscv64-gnu': 4.64.12 + '@rollup/rollup-linux-riscv64-musl': 4.64.12 + '@rollup/rollup-linux-s390x-gnu': 4.64.12 + '@rollup/rollup-linux-x64-gnu': 4.64.12 + '@rollup/rollup-linux-x64-musl': 4.64.12 + '@rollup/rollup-openbsd-x64': 4.64.12 + '@rollup/rollup-openharmony-arm64': 4.64.12 + '@rollup/rollup-win32-arm64-msvc': 4.64.12 + '@rollup/rollup-win32-ia32-msvc': 4.64.12 + '@rollup/rollup-win32-x64-gnu': 4.64.12 + '@rollup/rollup-win32-x64-msvc': 4.64.12 fsevents: 2.3.3 - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 @@ -4882,7 +4741,7 @@ snapshots: semver@6.3.1: {} - semver@7.8.4: {} + semver@7.8.5: {} set-cookie-parser@2.7.2: {} @@ -4942,7 +4801,7 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} stop-iteration-iterator@1.1.0: dependencies: @@ -4955,7 +4814,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 - string.prototype.matchall@4.0.12: + string.prototype.matchall@4.1.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -5002,16 +4861,6 @@ snapshots: strip-json-comments@3.1.1: {} - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 - ts-interface-checker: 0.1.13 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5020,63 +4869,23 @@ snapshots: tailwind-merge@2.6.1: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.19): - dependencies: - tailwindcss: 3.4.19 + tailwindcss@4.3.3: {} - tailwindcss@3.4.19: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.3 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.7 - lilconfig: 3.1.3 - micromatch: 4.0.8 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.1.1 - postcss: 8.5.15 - postcss-import: 15.1.0(postcss@8.5.15) - postcss-js: 4.1.0(postcss@8.5.15) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15) - postcss-nested: 6.2.0(postcss@8.5.15) - postcss-selector-parser: 6.1.2 - resolve: 1.22.12 - sucrase: 3.35.1 - transitivePeerDependencies: - - tsx - - yaml + tapable@2.3.3: {} - thenify-all@1.6.0: + tinyglobby@0.2.17: dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-interface-checker@0.1.13: {} - tslib@2.8.1: {} + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -5095,13 +4904,12 @@ snapshots: has-proto: 1.2.0 is-typed-array: 1.1.15 - typed-array-byte-offset@1.0.4: + typed-array-byte-offset@1.0.5: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 - has-proto: 1.2.0 is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 @@ -5114,13 +4922,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.61.1(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.61.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.61.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.4(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.72.0(@typescript-eslint/parser@8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.72.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.72.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5136,9 +4944,9 @@ snapshots: undici-types@7.24.6: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.3.4(browserslist@4.29.4): dependencies: - browserslist: 4.28.2 + browserslist: 4.29.4 escalade: 3.2.0 picocolors: 1.1.1 @@ -5146,41 +4954,38 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@18.3.29)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@18.3.31)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - use-sidecar@1.1.3(@types/react@18.3.29)(react@18.3.1): + use-sidecar@1.1.3(@types/react@18.3.31)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.29 + '@types/react': 18.3.31 - use-sync-external-store@1.6.0(react@18.3.1): + use-sync-external-store@1.7.0(react@18.3.1): dependencies: react: 18.3.1 - util-deprecate@1.0.2: {} - - vite@6.4.2(@types/node@25.9.1)(jiti@1.21.7): + vite@6.4.4(@types/node@25.9.9)(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.15 - rollup: 4.60.4 - tinyglobby: 0.2.16 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.31 + rollup: 4.64.12 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.1 + '@types/node': 25.9.9 fsevents: 2.3.3 - jiti: 1.21.7 - - void-elements@3.1.0: {} + jiti: 2.7.0 + lightningcss: 1.32.0 which-boxed-primitive@1.1.1: dependencies: @@ -5204,7 +5009,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 which-collection@1.0.2: dependencies: @@ -5213,7 +5018,7 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.22: + which-typed-array@1.1.24: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 @@ -5233,8 +5038,8 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.4.3): + zod-validation-error@4.0.2(zod@4.6.7): dependencies: - zod: 4.4.3 + zod: 4.6.7 - zod@4.4.3: {} + zod@4.6.7: {} diff --git a/client/ui/frontend/postcss.config.js b/client/ui/frontend/postcss.config.js deleted file mode 100644 index 2aa7205d4..000000000 --- a/client/ui/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/client/ui/frontend/src/components/Badge.tsx b/client/ui/frontend/src/components/Badge.tsx index a6f2e886a..05ad9dbfa 100644 --- a/client/ui/frontend/src/components/Badge.tsx +++ b/client/ui/frontend/src/components/Badge.tsx @@ -30,7 +30,7 @@ export const Badge = forwardRef(function Badge( ref={ref} className={cn( "relative top-px inline-flex items-center gap-1 rounded-full px-1.5 py-[0.15rem]", - "shrink-0 text-[0.64rem] font-semibold leading-none", + "shrink-0 text-[0.64rem] leading-none font-semibold", VARIANT_CLASSES[variant], className, )} diff --git a/client/ui/frontend/src/components/CopyToClipboard.tsx b/client/ui/frontend/src/components/CopyToClipboard.tsx index b3d5a8715..883890653 100644 --- a/client/ui/frontend/src/components/CopyToClipboard.tsx +++ b/client/ui/frontend/src/components/CopyToClipboard.tsx @@ -4,8 +4,8 @@ import { Check, Copy } from "lucide-react"; import { cn } from "@/lib/cn"; const VARIANT_HOVER = { - default: "group-hover/copy:[&_*]:text-nb-gray-300", - bright: "group-hover/copy:[&_*]:text-nb-gray-200", + default: "**:group-hover/copy:text-nb-gray-300", + bright: "**:group-hover/copy:text-nb-gray-200", } as const; type CopyToClipboardVariant = keyof typeof VARIANT_HOVER; @@ -80,7 +80,7 @@ export const CopyToClipboard = ({ aria-label={resolvedLabel} aria-live={"polite"} className={cn( - "group/copy wails-no-draggable pointer-events-auto inline-flex cursor-default items-center gap-2 rounded-sm text-start outline-none", + "group/copy wails-no-draggable pointer-events-auto inline-flex cursor-default items-center gap-2 rounded-xs text-start outline-hidden", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", className, )} @@ -88,8 +88,8 @@ export const CopyToClipboard = ({ @@ -104,7 +104,7 @@ export const CopyToClipboard = ({ - + @@ -163,10 +163,10 @@ const DropdownMenuRadioItem = React.forwardRef<
@@ -161,7 +161,7 @@ export function LanguagePicker() { aria-label={t("settings.general.language.search")} className={cn( "w-full bg-transparent text-xs text-nb-gray-100 placeholder:text-nb-gray-600 dark:placeholder:text-nb-gray-300", - "border-none outline-none", + "border-none outline-hidden", )} />
@@ -192,7 +192,7 @@ export function LanguagePicker() { value={`${lang.displayName} ${lang.englishName} ${lang.code}`} onSelect={() => void select(lang.code)} className={cn( - "my-0.5 flex cursor-default items-center gap-2 rounded-md px-2 py-2 outline-none", + "my-0.5 flex cursor-default items-center gap-2 rounded-md px-2 py-2 outline-hidden", "text-xs font-semibold text-nb-gray-200", "data-[selected=true]:bg-nb-gray-850 data-[selected=true]:text-nb-gray-50", )} @@ -221,7 +221,7 @@ export function LanguagePicker() { diff --git a/client/ui/frontend/src/components/ManagementServerSwitch.tsx b/client/ui/frontend/src/components/ManagementServerSwitch.tsx index 0083a767a..7d0cfb8a1 100644 --- a/client/ui/frontend/src/components/ManagementServerSwitch.tsx +++ b/client/ui/frontend/src/components/ManagementServerSwitch.tsx @@ -26,7 +26,7 @@ export const ManagementServerSwitch = ({ value, onChange, fullWidth = false }: P src={netbirdLogo} alt={""} aria-hidden={"true"} - className={"aspect-[31/23] h-[0.8rem] shrink-0"} + className={"aspect-31/23 h-[0.8rem] shrink-0"} /> {t("settings.general.management.cloud")} diff --git a/client/ui/frontend/src/components/Tooltip.tsx b/client/ui/frontend/src/components/Tooltip.tsx index 094ef9baf..0770501f1 100644 --- a/client/ui/frontend/src/components/Tooltip.tsx +++ b/client/ui/frontend/src/components/Tooltip.tsx @@ -95,8 +95,8 @@ export const Tooltip = ({ onPointerLeave={interactive ? scheduleClose : undefined} onPointerDownOutside={interactive ? undefined : (e) => e.preventDefault()} className={cn( - "z-50 select-none text-xs text-nb-gray-100 shadow-sm dark:shadow-lg", - "data-[state=delayed-open]:animate-in data-[state=closed]:animate-out", + "z-50 text-xs text-nb-gray-100 shadow-xs select-none dark:shadow-lg", + "data-[state=closed]:animate-out data-[state=delayed-open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=delayed-open]:fade-in-0", !interactive && "pointer-events-none", contentClassName ?? diff --git a/client/ui/frontend/src/components/VerticalTabs.tsx b/client/ui/frontend/src/components/VerticalTabs.tsx index bd1912662..ee215549d 100644 --- a/client/ui/frontend/src/components/VerticalTabs.tsx +++ b/client/ui/frontend/src/components/VerticalTabs.tsx @@ -49,7 +49,7 @@ const Trigger = forwardRef(function VerticalTab (function VerticalTab className={cn( "ms-2 shrink-0 transition-colors duration-150", "text-nb-gray-350 dark:text-nb-gray-400", - "group-data-[state=active]:text-nb-gray-100", + "group-data-[state=active]:text-nb-gray-100 dark:group-data-[state=active]:text-nb-gray-100", )} /> {title} {adornment && ( -
+
{adornment}
)} @@ -94,7 +94,7 @@ const Content = forwardRef(function Verti ); diff --git a/client/ui/frontend/src/components/buttons/Button.tsx b/client/ui/frontend/src/components/buttons/Button.tsx index 50e8c919d..39fab683a 100644 --- a/client/ui/frontend/src/components/buttons/Button.tsx +++ b/client/ui/frontend/src/components/buttons/Button.tsx @@ -16,9 +16,9 @@ interface ButtonProps extends ButtonHTMLAttributes, ButtonVar const buttonVariants = cva( [ "relative", - "cursor-default select-none whitespace-nowrap text-sm font-medium shadow-sm focus:z-10 focus:outline-none focus:ring-2", + "cursor-default text-sm font-medium whitespace-nowrap shadow-xs select-none focus:z-10 focus:ring-2 focus:outline-hidden", "inline-flex items-center justify-center gap-2 transition-colors focus:ring-offset-1", - "disabled:cursor-not-allowed disabled:opacity-40 dark:ring-offset-neutral-950/50 disabled:dark:text-nb-gray-300", + "disabled:cursor-not-allowed disabled:opacity-40 dark:ring-offset-neutral-950/50 dark:disabled:text-nb-gray-300", ], { variants: { @@ -28,7 +28,7 @@ const buttonVariants = cva( "dark:border-gray-700/30 dark:bg-nb-gray dark:text-gray-400 dark:hover:bg-zinc-800/50 dark:hover:text-nb-gray-50 dark:focus:ring-zinc-800/50", ], primary: [ - "dark:text-gray-100 dark:ring-offset-neutral-950/50 dark:focus:ring-netbird-600/50 enabled:dark:bg-netbird enabled:dark:hover:bg-netbird-500/80 enabled:dark:hover:text-nb-gray-50 disabled:dark:bg-nb-gray-900", + "dark:text-gray-100 dark:ring-offset-neutral-950/50 dark:focus:ring-netbird-600/50 dark:enabled:bg-netbird dark:enabled:hover:bg-netbird-500/80 dark:enabled:hover:text-nb-gray-50 dark:disabled:bg-nb-gray-900", "enabled:bg-netbird enabled:text-white enabled:hover:bg-netbird-500 enabled:focus:ring-netbird-400/50 disabled:bg-nb-gray-700", ], secondary: [ @@ -63,11 +63,11 @@ const buttonVariants = cva( ], tertiary: [ "border-neutral-200 bg-white text-neutral-900 hover:bg-neutral-100 hover:text-black focus:ring-neutral-200/50", - "dark:border-gray-700/40 dark:bg-white dark:text-gray-800 dark:hover:bg-neutral-200 dark:focus:ring-zinc-800/50 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300", + "dark:border-gray-700/40 dark:bg-white dark:text-gray-800 dark:hover:bg-neutral-200 dark:focus:ring-zinc-800/50 dark:disabled:bg-nb-gray-920 dark:disabled:text-nb-gray-300", ], white: [ - "border-white bg-white text-neutral-800 outline-none hover:bg-neutral-200 focus:ring-white/50 dark:text-gray-800 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300", - "disabled:dark:border-nb-gray-900 disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300", + "border-white bg-white text-neutral-800 outline-hidden hover:bg-neutral-200 focus:ring-white/50 dark:text-gray-800 dark:disabled:bg-nb-gray-920 dark:disabled:text-nb-gray-300", + "dark:disabled:border-nb-gray-900 dark:disabled:bg-nb-gray-900 dark:disabled:text-nb-gray-300", ], outline: [ "border-neutral-200 bg-white text-neutral-900 hover:bg-neutral-100 hover:text-black focus:ring-neutral-200/50", @@ -75,10 +75,10 @@ const buttonVariants = cva( ], "danger-outline": [ "bg-transparent text-red-600 enabled:hover:bg-red-50 enabled:focus:ring-red-200/50", - "dark:bg-transparent dark:text-red-500 enabled:dark:hover:border-red-800/50 enabled:hover:dark:bg-red-950/50 enabled:dark:focus:bg-red-950/40 enabled:dark:focus:ring-red-800/20", + "dark:bg-transparent dark:text-red-500 dark:enabled:hover:border-red-800/50 dark:enabled:hover:bg-red-950/50 dark:enabled:focus:bg-red-950/40 dark:enabled:focus:ring-red-800/20", ], "danger-text": [ - "rounded-sm border-transparent bg-transparent !px-0 !py-0 text-red-600 !shadow-none hover:text-red-700 focus:ring-red-500/30", + "rounded-xs border-transparent bg-transparent px-0! py-0! text-red-600 shadow-none! hover:text-red-700 focus:ring-red-500/30", "dark:border-transparent dark:bg-transparent dark:text-red-500 dark:ring-offset-neutral-950/50 dark:hover:text-red-600", ], "default-outline": [ @@ -92,12 +92,12 @@ const buttonVariants = cva( ], danger: [ "bg-red-600 text-red-50 hover:bg-red-700 focus:bg-red-700 focus:ring-red-700/20", - "dark:bg-red-600 dark:text-red-100 dark:hover:border-red-800/50 hover:dark:bg-red-700 dark:focus:bg-red-700 dark:focus:ring-red-700/20", + "dark:bg-red-600 dark:text-red-100 dark:hover:border-red-800/50 dark:hover:bg-red-700 dark:focus:bg-red-700 dark:focus:ring-red-700/20", ], }, size: { xs: "px-3.5 py-2.5 text-xs", - xs2: "px-4 py-[1.1rem] text-[0.78rem] leading-[0]", + xs2: "px-4 py-[1.1rem] text-[0.78rem] leading-0", sm: "px-4 py-[9px] text-sm", md: "px-4 py-[9px]", lg: "px-4 py-[9px] text-lg", @@ -109,7 +109,7 @@ const buttonVariants = cva( border: { 0: "border", 1: "border border-transparent", - 2: "border border-b-0 border-t-0", + 2: "border border-t-0 border-b-0", }, }, }, diff --git a/client/ui/frontend/src/components/buttons/IconButton.tsx b/client/ui/frontend/src/components/buttons/IconButton.tsx index 8d688cb0b..98c34250c 100644 --- a/client/ui/frontend/src/components/buttons/IconButton.tsx +++ b/client/ui/frontend/src/components/buttons/IconButton.tsx @@ -21,7 +21,7 @@ export const IconButton = forwardRef(function IconButt disabled={disabled} tabIndex={disabled ? -1 : 0} className={cn( - "flex h-10 w-10 cursor-default items-center justify-center rounded-lg outline-none", + "flex h-10 w-10 cursor-default items-center justify-center rounded-lg outline-hidden", "text-nb-gray-400 hover:bg-nb-gray-900 hover:text-nb-gray-300", isFocusVisible && "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", diff --git a/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx b/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx index caf98c2c8..388284537 100644 --- a/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx +++ b/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx @@ -18,13 +18,13 @@ export const ConfirmDialog = forwardRef(func aria-label={ariaLabel} aria-labelledby={ariaLabelledBy} className={ - "wails-draggable static m-0 flex max-h-none w-full max-w-none select-none flex-col items-center border-0 bg-transparent p-0 text-inherit" + "wails-draggable static m-0 flex max-h-none w-full max-w-none flex-col items-center border-0 bg-transparent p-0 text-inherit select-none" } >
diff --git a/client/ui/frontend/src/components/dialog/Dialog.tsx b/client/ui/frontend/src/components/dialog/Dialog.tsx index 3d0d47fc2..43584607f 100644 --- a/client/ui/frontend/src/components/dialog/Dialog.tsx +++ b/client/ui/frontend/src/components/dialog/Dialog.tsx @@ -65,8 +65,8 @@ export const Content = forwardRef, Co , Co {showClose && ( @@ -119,7 +119,7 @@ export const Title = forwardRef< *]:w-full sm:[&>*]:w-auto", + "*:w-full sm:*:w-auto", "px-8 pt-6", className, )} diff --git a/client/ui/frontend/src/components/dialog/DialogDescription.tsx b/client/ui/frontend/src/components/dialog/DialogDescription.tsx index 3d6ecbba7..95a235fbb 100644 --- a/client/ui/frontend/src/components/dialog/DialogDescription.tsx +++ b/client/ui/frontend/src/components/dialog/DialogDescription.tsx @@ -20,7 +20,7 @@ export const DialogDescription = ({ className, align = "center", }: DialogDescriptionProps) => ( -

+

{children}

); diff --git a/client/ui/frontend/src/components/dialog/DialogHeading.tsx b/client/ui/frontend/src/components/dialog/DialogHeading.tsx index 52eb0c369..1cec1f8c7 100644 --- a/client/ui/frontend/src/components/dialog/DialogHeading.tsx +++ b/client/ui/frontend/src/components/dialog/DialogHeading.tsx @@ -25,7 +25,7 @@ export const DialogHeading = ({

{ return (
diff --git a/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx b/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx index a40959a23..dddd35ae6 100644 --- a/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx +++ b/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx @@ -26,7 +26,7 @@ export const DaemonUnavailableOverlay = () => { return (
diff --git a/client/ui/frontend/src/components/empty-state/NoResults.tsx b/client/ui/frontend/src/components/empty-state/NoResults.tsx index cf0995b37..8b3ce2063 100644 --- a/client/ui/frontend/src/components/empty-state/NoResults.tsx +++ b/client/ui/frontend/src/components/empty-state/NoResults.tsx @@ -16,7 +16,7 @@ export const NoResults = ({ icon = FunnelXIcon, title, description }: Props) => icon={icon} title={title ?? t("common.noResults.title")} description={description ?? t("common.noResults.description")} - className={"pointer-events-none relative -top-[3.8rem]"} + className={"pointer-events-none relative top-[-3.8rem]"} /> ); }; diff --git a/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx b/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx index 2bcf70376..fd5330835 100644 --- a/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx +++ b/client/ui/frontend/src/components/empty-state/NotConnectedState.tsx @@ -5,7 +5,7 @@ import { EmptyState } from "./EmptyState"; export const NotConnectedState = () => { const { t } = useTranslation(); return ( -
+
@@ -138,7 +138,7 @@ function InputSuffixSlot({ return (
@@ -349,7 +349,7 @@ export const Input = forwardRef(function Input( {icon && } -
+
(function SearchIn {...props} className={cn( "w-full bg-transparent text-sm text-nb-gray-200 placeholder:text-nb-gray-600 dark:placeholder:text-nb-gray-400", - "border-none outline-none", + "border-none outline-hidden", disabled && "cursor-not-allowed", className, )} @@ -45,7 +45,7 @@ export const SearchInput = forwardRef(function SearchIn className={cn( "shrink-0 select-none", "inline-flex items-center justify-center", - "h-5 min-w-[20px] rounded px-1.5", + "h-5 min-w-[20px] rounded-sm px-1.5", "border border-nb-gray-850 bg-nb-gray-920", "text-[10px] font-medium text-nb-gray-400", "wails-no-draggable", diff --git a/client/ui/frontend/src/components/inputs/Select.tsx b/client/ui/frontend/src/components/inputs/Select.tsx index d9c7512d2..0938df091 100644 --- a/client/ui/frontend/src/components/inputs/Select.tsx +++ b/client/ui/frontend/src/components/inputs/Select.tsx @@ -49,7 +49,7 @@ export function Select({ "inline-flex h-[40px] min-w-[160px] items-center gap-2 px-3", "rounded-md border bg-white dark:bg-nb-gray-900", "border-neutral-200 dark:border-nb-gray-700", - "cursor-default text-xs font-semibold text-nb-gray-100 outline-none", + "cursor-default text-xs font-semibold text-nb-gray-100 outline-hidden", "hover:border-nb-gray-700 data-[state=open]:border-nb-gray-700 dark:hover:border-nb-gray-600 dark:data-[state=open]:border-nb-gray-600", isFocusVisible && "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", @@ -76,7 +76,7 @@ export function Select({ align={"start"} sideOffset={6} className={ - "w-[var(--radix-dropdown-menu-trigger-width)] border-nb-gray-850 bg-nb-gray-920" + "w-(--radix-dropdown-menu-trigger-width) border-nb-gray-850 bg-nb-gray-920" } > onChange(v as T)}> diff --git a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx index b6331ee2d..292e35e14 100644 --- a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx +++ b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx @@ -36,7 +36,7 @@ export default function FancyToggleSwitch({ if (loading) { const shimmer = - "text-transparent select-none rounded bg-nb-gray-920 box-decoration-clone animate-pulse"; + "text-transparent select-none rounded-sm bg-nb-gray-920 box-decoration-clone animate-pulse"; return (
{ value={value} className={cn( "relative inline-flex items-center justify-center gap-1 rounded-md px-3.5 py-2 text-xs font-semibold", - "cursor-default outline-none", + "cursor-default outline-hidden", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", active ? "text-nb-gray-100" @@ -31,7 +31,7 @@ export const SwitchItem = ({ value, children, className }: Props) => { diff --git a/client/ui/frontend/src/components/switches/ToggleSwitch.tsx b/client/ui/frontend/src/components/switches/ToggleSwitch.tsx index 6862f85d8..7ef097632 100644 --- a/client/ui/frontend/src/components/switches/ToggleSwitch.tsx +++ b/client/ui/frontend/src/components/switches/ToggleSwitch.tsx @@ -36,9 +36,9 @@ const switchVariants = cva("", { }, "thumb-size": { default: - "h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 rtl:data-[state=checked]:-translate-x-5", - small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px] data-[state=unchecked]:translate-x-0 rtl:data-[state=checked]:-translate-x-[17px]", - large: "h-[30px] w-[30px] data-[state=checked]:translate-x-[31px] data-[state=unchecked]:translate-x-[1px] rtl:data-[state=checked]:-translate-x-[31px] rtl:data-[state=unchecked]:-translate-x-[1px]", + "h-5 w-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=checked]:rtl:-translate-x-5", + small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px] data-[state=unchecked]:translate-x-0 data-[state=checked]:rtl:translate-x-[-17px]", + large: "h-[30px] w-[30px] data-[state=checked]:translate-x-[31px] data-[state=unchecked]:translate-x-px data-[state=checked]:rtl:translate-x-[-31px] data-[state=unchecked]:rtl:-translate-x-px", }, }, }); @@ -52,7 +52,7 @@ const ToggleSwitch = React.forwardRef< disabled={disabled} tabIndex={disabled ? -1 : 0} className={cn( - "wails-no-draggable peer inline-flex shrink-0 cursor-default items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940 disabled:cursor-not-allowed disabled:opacity-50", + "wails-no-draggable peer inline-flex shrink-0 cursor-default items-center rounded-full border-2 border-transparent transition-colors focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className, switchVariants({ size, variant }), )} diff --git a/client/ui/frontend/src/components/typography/Label.tsx b/client/ui/frontend/src/components/typography/Label.tsx index a8e1a446f..fc83dfd0c 100644 --- a/client/ui/frontend/src/components/typography/Label.tsx +++ b/client/ui/frontend/src/components/typography/Label.tsx @@ -4,7 +4,7 @@ import { type ComponentPropsWithoutRef, forwardRef, type Ref } from "react"; import { cn } from "@/lib/cn"; const labelVariants = cva( - "mb-1.5 inline-block flex items-center gap-2 text-sm font-medium leading-none tracking-wider peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-nb-gray-100", + "mb-1.5 flex items-center gap-2 text-sm leading-none font-medium tracking-wider peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-nb-gray-100", ); type LabelProps = ComponentPropsWithoutRef & @@ -20,7 +20,7 @@ export const Label = forwardRef(function Label( const classes = cn( labelVariants(), className, - "select-none transition-all duration-300", + "transition-all duration-300 select-none", disabled && "pointer-events-none opacity-30", ); diff --git a/client/ui/frontend/src/globals.css b/client/ui/frontend/src/globals.css index bfdabeb31..ac9813c12 100644 --- a/client/ui/frontend/src/globals.css +++ b/client/ui/frontend/src/globals.css @@ -1,22 +1,280 @@ -@font-face { - font-family: "Inter Variable"; - font-style: normal; - font-weight: 100 900; - src: url("./assets/fonts/inter-variable.ttf") format("truetype"); +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --font-*: initial; + --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono Variable", ui-monospace, monospace; + + --color-nb-gray-50: rgb(var(--nb-gray-50)); + --color-nb-gray-100: rgb(var(--nb-gray-100)); + --color-nb-gray-200: rgb(var(--nb-gray-200)); + --color-nb-gray-250: rgb(var(--nb-gray-250)); + --color-nb-gray-300: rgb(var(--nb-gray-300)); + --color-nb-gray-350: rgb(var(--nb-gray-350)); + --color-nb-gray-400: rgb(var(--nb-gray-400)); + --color-nb-gray-500: rgb(var(--nb-gray-500)); + --color-nb-gray-600: rgb(var(--nb-gray-600)); + --color-nb-gray-700: rgb(var(--nb-gray-700)); + --color-nb-gray-800: rgb(var(--nb-gray-800)); + --color-nb-gray-850: rgb(var(--nb-gray-850)); + --color-nb-gray-900: rgb(var(--nb-gray-900)); + --color-nb-gray-910: rgb(var(--nb-gray-910)); + --color-nb-gray-920: rgb(var(--nb-gray-920)); + --color-nb-gray-925: rgb(var(--nb-gray-925)); + --color-nb-gray-930: rgb(var(--nb-gray-930)); + --color-nb-gray-935: rgb(var(--nb-gray-935)); + --color-nb-gray-940: rgb(var(--nb-gray-940)); + --color-nb-gray-950: rgb(var(--nb-gray-950)); + --color-nb-gray-960: rgb(var(--nb-gray-960)); + --color-nb-gray: rgb(var(--nb-gray-DEFAULT)); + + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + + --color-red-50: #fdf2f2; + --color-red-100: #fde8e8; + --color-red-200: #fbd5d5; + --color-red-300: #f8b4b4; + --color-red-400: #f98080; + --color-red-500: #f05252; + --color-red-600: #e02424; + --color-red-700: #c81e1e; + --color-red-800: #9b1c1c; + --color-red-900: #771d1d; + + --color-yellow-50: #fdfdea; + --color-yellow-100: #fdf6b2; + --color-yellow-200: #fce96a; + --color-yellow-300: #faca15; + --color-yellow-400: #e3a008; + --color-yellow-500: #c27803; + --color-yellow-600: #9f580a; + --color-yellow-700: #8e4b10; + --color-yellow-800: #723b13; + --color-yellow-900: #633112; + + --color-green-50: #f3faf7; + --color-green-100: #def7ec; + --color-green-200: #bcf0da; + --color-green-300: #84e1bc; + --color-green-400: #31c48d; + --color-green-500: #0e9f6e; + --color-green-600: #057a55; + --color-green-700: #046c4e; + --color-green-800: #03543f; + --color-green-900: #014737; + + --color-blue-50: #ebf5ff; + --color-blue-100: #e1effe; + --color-blue-200: #c3ddfd; + --color-blue-300: #a4cafe; + --color-blue-400: #76a9fa; + --color-blue-500: #3f83f8; + --color-blue-600: #1c64f2; + --color-blue-700: #1a56db; + --color-blue-800: #1e429f; + --color-blue-900: #233876; + + --color-indigo-50: #f0f5ff; + --color-indigo-100: #e5edff; + --color-indigo-200: #cddbfe; + --color-indigo-300: #b4c6fc; + --color-indigo-400: #8da2fb; + --color-indigo-500: #6875f5; + --color-indigo-600: #5850ec; + --color-indigo-700: #5145cd; + --color-indigo-800: #42389d; + --color-indigo-900: #362f78; + + --color-purple-50: #f6f5ff; + --color-purple-100: #edebfe; + --color-purple-200: #dcd7fe; + --color-purple-300: #cabffd; + --color-purple-400: #ac94fa; + --color-purple-500: #9061f9; + --color-purple-600: #7e3af2; + --color-purple-700: #6c2bd9; + --color-purple-800: #5521b5; + --color-purple-900: #4a1d96; + + --color-pink-50: #fdf2f8; + --color-pink-100: #fce8f3; + --color-pink-200: #fad1e8; + --color-pink-300: #f8b4d9; + --color-pink-400: #f17eb8; + --color-pink-500: #e74694; + --color-pink-600: #d61f69; + --color-pink-700: #bf125d; + --color-pink-800: #99154b; + --color-pink-900: #751a3d; + + --color-netbird-50: #fff6ed; + --color-netbird-100: #feecd6; + --color-netbird-150: #ffdfb8; + --color-netbird-200: #ffd4a6; + --color-netbird-300: #fab677; + --color-netbird-400: #f68330; + --color-netbird-500: #f46d1b; + --color-netbird-600: #e55311; + --color-netbird-700: #be3e10; + --color-netbird-800: #973215; + --color-netbird-900: #7a2b14; + --color-netbird-950: #421308; + --color-netbird: #f68330; + + /* Tailwind 4 rebuilt its default palette in OKLCH, which shifts hues such as + sky-400 and orange-400 noticeably. These are the Tailwind 3 values for the + families the UI still uses from the default palette, so colors stay as they + were before the upgrade. */ + --color-neutral-50: #fafafa; + --color-neutral-100: #f5f5f5; + --color-neutral-200: #e5e5e5; + --color-neutral-300: #d4d4d4; + --color-neutral-400: #a3a3a3; + --color-neutral-500: #737373; + --color-neutral-600: #525252; + --color-neutral-700: #404040; + --color-neutral-800: #262626; + --color-neutral-900: #171717; + --color-neutral-950: #0a0a0a; + + --color-zinc-50: #fafafa; + --color-zinc-100: #f4f4f5; + --color-zinc-200: #e4e4e7; + --color-zinc-300: #d4d4d8; + --color-zinc-400: #a1a1aa; + --color-zinc-500: #71717a; + --color-zinc-600: #52525b; + --color-zinc-700: #3f3f46; + --color-zinc-800: #27272a; + --color-zinc-900: #18181b; + --color-zinc-950: #09090b; + + --color-sky-50: #f0f9ff; + --color-sky-100: #e0f2fe; + --color-sky-200: #bae6fd; + --color-sky-300: #7dd3fc; + --color-sky-400: #38bdf8; + --color-sky-500: #0ea5e9; + --color-sky-600: #0284c7; + --color-sky-700: #0369a1; + --color-sky-800: #075985; + --color-sky-900: #0c4a6e; + --color-sky-950: #082f49; + + --color-orange-50: #fff7ed; + --color-orange-100: #ffedd5; + --color-orange-200: #fed7aa; + --color-orange-300: #fdba74; + --color-orange-400: #fb923c; + --color-orange-500: #f97316; + --color-orange-600: #ea580c; + --color-orange-700: #c2410c; + --color-orange-800: #9a3412; + --color-orange-900: #7c2d12; + --color-orange-950: #431407; + + --color-amber-50: #fffbeb; + --color-amber-100: #fef3c7; + --color-amber-200: #fde68a; + --color-amber-300: #fcd34d; + --color-amber-400: #fbbf24; + --color-amber-500: #f59e0b; + --color-amber-600: #d97706; + --color-amber-700: #b45309; + --color-amber-800: #92400e; + --color-amber-900: #78350f; + --color-amber-950: #451a03; + + --background-image-conic-netbird: conic-gradient( + from 0deg, + #e55311 0%, + #f68330 10%, + #e55311 20%, + #e55311 100% + ); + + --animate-ping-slow: ping-slow 2s cubic-bezier(0, 0, 0.2, 1) infinite; + --animate-pulse-slow: pulse-reverse 2s cubic-bezier(0.5, 0, 0.6, 1) infinite; + --animate-pulse-slower: pulse-reverse 3s cubic-bezier(0.5, 0, 0.6, 1) infinite; + --animate-spin-slow: spin-slow 2s linear infinite; + + @keyframes pulse-reverse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } + } + @keyframes spin-slow { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + @keyframes ping-slow { + 0% { + transform: scale(1); + opacity: 1; + } + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } } -@font-face { - font-family: "JetBrains Mono Variable"; - font-style: normal; - font-weight: 100 800; - src: url("./assets/fonts/jetbrains-mono-variable.ttf") format("truetype"); +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } } -@tailwind base; -@tailwind components; -@tailwind utilities; +@layer base { + @font-face { + font-family: "Inter Variable"; + font-style: normal; + font-weight: 100 900; + src: url("./assets/fonts/inter-variable.ttf") format("truetype"); + } -/* nb-gray channels (space-separated RGB) consumed by tailwind.config.ts. + @font-face { + font-family: "JetBrains Mono Variable"; + font-style: normal; + font-weight: 100 800; + src: url("./assets/fonts/jetbrains-mono-variable.ttf") format("truetype"); + } +} + +/* nb-gray channels (space-separated RGB) consumed by the @theme block above. :root is the inverted light ramp — high stops are surfaces (near-white), low stops are text (near-black); .dark restores the original dark ramp. Surfaces stay darker than content cards to preserve their separation. */ diff --git a/client/ui/frontend/src/modules/error/ErrorDialog.tsx b/client/ui/frontend/src/modules/error/ErrorDialog.tsx index 0be13b86e..3b5e27b24 100644 --- a/client/ui/frontend/src/modules/error/ErrorDialog.tsx +++ b/client/ui/frontend/src/modules/error/ErrorDialog.tsx @@ -54,7 +54,7 @@ export default function ErrorDialog() { {/* select-text: the message often names a path, a flag or an address the user needs to act on. */} - + {message} @@ -71,7 +71,7 @@ export default function ErrorDialog() { aria-label={t("common.copy")} > {command} diff --git a/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx b/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx index 845252dcf..7c7d4d394 100644 --- a/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx +++ b/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx @@ -245,7 +245,7 @@ export const MainConnectionStatusSwitch = () => { role={"status"} aria-live={"polite"} className={ - "wails-no-draggable mb-1 select-none text-sm font-medium tracking-wide text-nb-gray-200 transition-colors duration-300" + "wails-no-draggable mb-1 text-sm font-medium tracking-wide text-nb-gray-200 transition-colors duration-300 select-none" } > {t(STATUS_KEY[connState])} @@ -257,7 +257,7 @@ export const MainConnectionStatusSwitch = () => { tabIndex={show && fqdn ? 0 : -1} className={cn( "mt-1 max-h-[1em] min-h-[1em] max-w-full transition-opacity duration-300", - "relative start-[0.55rem]", + "relative inset-s-[0.55rem]", show && fqdn ? "opacity-100" : "pointer-events-none opacity-0", )} > @@ -289,7 +289,7 @@ const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boole tabIndex={show && ip ? 0 : -1} className={cn( "mt-1 max-h-[1em] min-h-[1em] transition-opacity duration-300", - "relative start-[0.55rem]", + "relative inset-s-[0.55rem]", show && ip ? "opacity-100" : "pointer-events-none opacity-0", )} > @@ -317,7 +317,7 @@ const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boole aria-haspopup={"dialog"} aria-expanded={open} className={cn( - "group relative inline-flex cursor-default items-center rounded-sm outline-none", + "group relative inline-flex cursor-default items-center rounded-xs outline-hidden", isFocusVisible && "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "transition-colors", @@ -336,7 +336,7 @@ const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boole size={14} aria-hidden={"true"} className={cn( - "absolute -end-5 top-1/2 -translate-y-1/2", + "absolute -inset-e-5 top-1/2 -translate-y-1/2", "shrink-0 text-nb-gray-300 transition-colors", "group-hover:text-nb-gray-200", "group-data-[state=open]:text-nb-gray-200", @@ -351,9 +351,9 @@ const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boole sideOffset={6} onOpenAutoFocus={(e) => e.preventDefault()} className={cn( - "z-50 min-w-64 max-w-[280px] overflow-hidden", + "z-50 max-w-[280px] min-w-64 overflow-hidden", "rounded-lg border border-nb-gray-900 bg-nb-gray-935", - "p-1 text-nb-gray-200 shadow-lg outline-none", + "p-1 text-nb-gray-200 shadow-lg outline-hidden", "flex flex-col", )} > @@ -391,9 +391,9 @@ const IpRow = ({ value }: { value: string }) => { "group/iprow relative flex items-center justify-between gap-3", "rounded-md px-2 py-1.5 text-start", "text-nb-gray-200 hover:bg-nb-gray-900 hover:text-nb-gray-50", - "cursor-default outline-none transition-colors", + "cursor-default outline-hidden transition-colors", isFocusVisible && - "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-nb-gray-50/60", + "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-inset", )} > {value} diff --git a/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx b/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx index 3e6281352..3396d3b54 100644 --- a/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx +++ b/client/ui/frontend/src/modules/main/MainExitNodeSwitcher.tsx @@ -81,8 +81,8 @@ export const MainExitNodeSwitcher = () => { }} style={{ width: "var(--radix-popover-trigger-width)" }} className={cn( - "wails-no-draggable z-50 select-none overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg", - "data-[state=open]:animate-in data-[state=closed]:animate-out", + "wails-no-draggable z-50 overflow-hidden rounded-lg border border-nb-gray-900 bg-nb-gray-935 p-1 text-nb-gray-200 shadow-lg select-none", + "data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2", @@ -93,12 +93,16 @@ export const MainExitNodeSwitcher = () => { loop shouldFilter={false} onKeyDown={(e) => e.stopPropagation()} - className={"outline-none focus:outline-none focus-visible:outline-none"} + className={ + "outline-hidden focus:outline-hidden focus-visible:outline-hidden" + } > handleSelect(NONE_VALUE)} /> {hasAny &&
} @@ -122,7 +126,7 @@ export const MainExitNodeSwitcher = () => { @@ -161,7 +165,7 @@ const ExitNodeTriggerCard = forwardRef( tabIndex={0} disabled={disabled} className={cn( - "flex w-full items-center gap-3 rounded-xl p-2.5 pe-5 text-start outline-none", + "flex w-full items-center gap-3 rounded-xl p-2.5 pe-5 text-start outline-hidden", "border border-nb-gray-800 bg-nb-gray-940 dark:border-nb-gray-920", "transition-colors duration-150", "wails-no-draggable", @@ -219,7 +223,7 @@ const NoneRow = ({ isActive, onSelect }: NoneRowProps) => { onSelect={onSelect} className={cn( "flex items-center gap-2 px-2 py-2 pe-3", - "cursor-default rounded-md text-sm outline-none", + "cursor-default rounded-md text-sm outline-hidden", "data-[selected=true]:bg-nb-gray-900", )} > @@ -244,7 +248,7 @@ const ExitNodeRow = ({ id, label, isActive, onSelect }: ExitNodeRowProps) => ( onSelect={onSelect} className={cn( "flex items-center gap-2 px-2 py-2 pe-3", - "cursor-default rounded-md text-sm outline-none", + "cursor-default rounded-md text-sm outline-hidden", "data-[selected=true]:bg-nb-gray-900", )} > diff --git a/client/ui/frontend/src/modules/main/MainHeader.tsx b/client/ui/frontend/src/modules/main/MainHeader.tsx index 6aa5b7993..d560666f3 100644 --- a/client/ui/frontend/src/modules/main/MainHeader.tsx +++ b/client/ui/frontend/src/modules/main/MainHeader.tsx @@ -89,7 +89,7 @@ export const MainHeader = () => { align={pinSettingsRight && dir === "rtl" ? "start" : "end"} sideOffset={8} className={ - "min-w-52 select-none data-[state=closed]:!animate-none data-[state=closed]:!duration-0" + "min-w-52 select-none data-[state=closed]:animate-none! data-[state=closed]:duration-0!" } > {updateAvailable && ( @@ -141,7 +141,7 @@ export const MainHeader = () => { {
{settingsSlot} diff --git a/client/ui/frontend/src/modules/main/advanced/Navigation.tsx b/client/ui/frontend/src/modules/main/advanced/Navigation.tsx index 1f0f527d1..6aa071906 100644 --- a/client/ui/frontend/src/modules/main/advanced/Navigation.tsx +++ b/client/ui/frontend/src/modules/main/advanced/Navigation.tsx @@ -110,10 +110,10 @@ export const Navigation = () => { className={cn( "group relative flex flex-1 items-center justify-center", "gap-2.5 px-5 py-3.5", - "outline-none transition-all", + "outline-hidden transition-all", isFirst && "rounded-ss-xl", isLast && "rounded-se-xl", - "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-nb-gray-50/60", + "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-inset", isActive ? "text-netbird" : "text-nb-gray-400 hover:text-nb-gray-300", isDisabled ? "cursor-not-allowed opacity-50" : "cursor-default", )} diff --git a/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx b/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx index 3686275f0..d9a871067 100644 --- a/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx +++ b/client/ui/frontend/src/modules/main/advanced/networks/NetworkFilters.tsx @@ -42,7 +42,7 @@ export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) => className={cn( "inline-flex h-9 items-center gap-1.5 rounded-md px-2", "text-sm text-nb-gray-200", - "outline-none transition-colors duration-150 hover:bg-nb-gray-900 data-[state=open]:bg-nb-gray-900", + "outline-hidden transition-colors duration-150 hover:bg-nb-gray-900 data-[state=open]:bg-nb-gray-900", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "disabled:pointer-events-none disabled:opacity-50", "wails-no-draggable cursor-default", @@ -54,7 +54,7 @@ export const NetworkFilters = ({ value, onChange, counts, disabled }: Props) => - + {filters.map((f) => { const checked = f.value === value; return ( diff --git a/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx b/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx index da8028155..6f2f3a710 100644 --- a/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx +++ b/client/ui/frontend/src/modules/main/advanced/networks/Networks.tsx @@ -194,7 +194,7 @@ export const Networks = () => { @@ -213,7 +213,7 @@ export const Networks = () => { "border-t border-nb-gray-910", )} > - + {t("networks.bulk.selectionCount", { selected: selectedInView, total: filtered.length, @@ -228,7 +228,7 @@ export const Networks = () => { "inline-flex h-8 items-center rounded-md px-3", "text-xs font-medium text-nb-gray-100", "border border-nb-gray-900 bg-nb-gray-920 hover:border-nb-gray-850 hover:bg-nb-gray-910", - "wails-no-draggable cursor-pointer outline-none transition-colors", + "wails-no-draggable cursor-pointer outline-hidden transition-colors", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", )} > @@ -349,7 +349,7 @@ const NetworkRow = ({ network: n, index, onKeyDown, onToggle, setRowRef }: Netwo return (
onToggle(n.id, n.selected)} onKeyDown={handleKey} className={cn( - "absolute inset-0 cursor-pointer outline-none", - "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-nb-gray-50/60", + "absolute inset-0 cursor-pointer outline-hidden", + "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-inset", )} /> @@ -401,7 +401,7 @@ const ResourceIconBadge = ({ type }: { type: ResourceType }) => {
{ const { t } = useTranslation(); return ( <> -
+
{t("networks.ips.heading")}
    @@ -496,7 +496,7 @@ const ResolvedIpsTooltip = ({ ips }: { ips: string[] }) => { {ip} @@ -516,7 +516,7 @@ type ToggleProps = { const NetworkToggle = ({ checked, mixed }: ToggleProps) => { const checkedTranslate = checked - ? "translate-x-[1.125rem] rtl:-translate-x-[1.125rem]" + ? "translate-x-4.5 rtl:-translate-x-4.5" : "translate-x-0.5 rtl:-translate-x-0.5"; return ( { className={cn( "flex h-8 w-8 shrink-0 items-center justify-center rounded-md", "text-nb-gray-300 hover:bg-nb-gray-910 hover:text-nb-gray-100", - "cursor-default outline-none transition-colors", + "cursor-default outline-hidden transition-colors", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "wails-no-draggable", )} @@ -241,7 +241,7 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => { className={cn( "flex h-8 w-8 shrink-0 items-center justify-center rounded-md", "text-nb-gray-300 hover:bg-nb-gray-910 hover:text-nb-gray-100", - "cursor-default outline-none transition-colors", + "cursor-default outline-hidden transition-colors", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "wails-no-draggable", "disabled:opacity-50 disabled:hover:bg-transparent", @@ -266,7 +266,7 @@ export const PeerDetailPanel = ({ transition = DEFAULT_TRANSITION }: Props) => { @@ -317,7 +317,7 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => { @@ -335,7 +335,7 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => { @@ -408,7 +408,7 @@ const PeerDetails = ({ peer, now }: { peer: PeerStatus; now: number }) => { @@ -446,7 +446,7 @@ const IceRow = ({ icon, baseLabel, type, endpoint }: IceRowProps) => { @@ -474,11 +474,11 @@ const ResourcesPopover = ({ networks }: { networks: string[] }) => { aria-haspopup={"dialog"} aria-expanded={open} className={cn( - "inline-flex shrink-0 items-center gap-1 rounded", + "inline-flex shrink-0 items-center gap-1 rounded-sm", "bg-nb-gray-930 hover:bg-nb-gray-910/80 data-[state=open]:bg-nb-gray-910", "border border-nb-gray-900", - "py-1 pe-2 ps-2.5 text-xs font-medium text-nb-gray-300", - "wails-no-draggable cursor-default outline-none transition-all", + "py-1 ps-2.5 pe-2 text-xs font-medium text-nb-gray-300", + "wails-no-draggable cursor-default outline-hidden transition-all", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", )} > @@ -497,9 +497,9 @@ const ResourcesPopover = ({ networks }: { networks: string[] }) => { sideOffset={6} onOpenAutoFocus={(e) => e.preventDefault()} className={cn( - "z-50 max-h-72 min-w-64 max-w-[280px] overflow-auto", + "z-50 max-h-72 max-w-[280px] min-w-64 overflow-auto", "rounded-lg border border-nb-gray-900 bg-nb-gray-935", - "p-1 text-nb-gray-200 shadow-lg outline-none", + "p-1 text-nb-gray-200 shadow-lg outline-hidden", "flex flex-col", )} > @@ -539,9 +539,9 @@ const ResourceRow = ({ value }: { value: string }) => { "group/resourcerow relative flex items-center justify-between gap-3", "rounded-md px-2 py-1.5 text-start", "text-nb-gray-200 hover:bg-nb-gray-900 hover:text-nb-gray-50", - "cursor-default outline-none transition-colors", + "cursor-default outline-hidden transition-colors", isFocusVisible && - "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-nb-gray-50/60", + "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-inset", )} > {value} @@ -560,7 +560,7 @@ const TruncatedRowValue = ({ value, mono }: { value: string; mono?: boolean }) = text={value} dir={mono ? "ltr" : "auto"} className={cn( - "inline-block min-w-0 max-w-[260px] truncate align-middle", + "inline-block max-w-[260px] min-w-0 truncate align-middle", mono && "font-mono", )} /> diff --git a/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx b/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx index 524bae5d4..92cbe548e 100644 --- a/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx +++ b/client/ui/frontend/src/modules/main/advanced/peers/PeerFilters.tsx @@ -42,7 +42,7 @@ export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => { className={cn( "inline-flex h-9 items-center gap-1.5 rounded-md px-2", "text-sm text-nb-gray-200", - "outline-none transition-colors duration-150 hover:bg-nb-gray-900 data-[state=open]:bg-nb-gray-900", + "outline-hidden transition-colors duration-150 hover:bg-nb-gray-900 data-[state=open]:bg-nb-gray-900", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", "disabled:pointer-events-none disabled:opacity-50", "wails-no-draggable cursor-default", @@ -54,7 +54,7 @@ export const PeerFilters = ({ value, onChange, counts, disabled }: Props) => { - + {filters.map((f) => { const checked = f.value === value; return ( diff --git a/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx b/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx index 79a02a471..71ca4d103 100644 --- a/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx +++ b/client/ui/frontend/src/modules/main/advanced/peers/Peers.tsx @@ -148,7 +148,7 @@ export const Peers = () => { @@ -280,7 +280,7 @@ const PeerRow = ({ peer, index, onKeyDown, onSelect, setRowRef }: PeerRowProps) return (
    onSelect(peer)} onKeyDown={handleKey} className={cn( - "absolute inset-0 cursor-default outline-none", - "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-nb-gray-50/60", + "absolute inset-0 cursor-default outline-hidden", + "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-inset", )} /> diff --git a/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx b/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx index 70d45acc5..0eaf3e88f 100644 --- a/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfileAvatar.tsx @@ -62,7 +62,7 @@ export const ProfileAvatar = forwardRef(function Profi type={type} className={cn( "inline-grid place-items-center rounded-full bg-nb-gray-900 p-0 text-center", - "cursor-default outline-none", + "cursor-default outline-hidden", "transition-colors duration-150 hover:bg-nb-gray-850", "data-[state=open]:bg-nb-gray-850", className, diff --git a/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx b/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx index bd0f1e07c..dd6ad3f73 100644 --- a/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx @@ -127,8 +127,8 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { listRef.current?.focus(); }} className={cn( - "wails-no-draggable z-50 min-w-64 select-none overflow-hidden rounded-lg border border-nb-gray-800 bg-nb-gray-950 p-1 text-nb-gray-200 shadow-lg dark:border-nb-gray-900 dark:bg-nb-gray-935", - "data-[state=open]:animate-in data-[state=closed]:animate-out", + "wails-no-draggable z-50 min-w-64 overflow-hidden rounded-lg border border-nb-gray-800 bg-nb-gray-950 p-1 text-nb-gray-200 shadow-lg select-none dark:border-nb-gray-900 dark:bg-nb-gray-935", + "data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[side=bottom]:origin-top data-[side=top]:origin-bottom", @@ -142,12 +142,16 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { loop shouldFilter={false} onKeyDown={(e) => e.stopPropagation()} - className={"outline-none focus:outline-none focus-visible:outline-none"} + className={ + "outline-hidden focus:outline-hidden focus-visible:outline-hidden" + } > {sortedProfiles.length > 0 && ( <> @@ -169,7 +173,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { @@ -191,7 +195,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { disabled={!onManageProfiles} className={cn( "flex items-center gap-2 px-2 py-1.5", - "cursor-default rounded-md text-sm outline-none", + "cursor-default rounded-md text-sm outline-hidden", "data-[selected=true]:bg-nb-gray-900", "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50", )} @@ -240,13 +244,13 @@ const ProfileTriggerSkeleton = () => ( role={"status"} aria-busy={"true"} aria-live={"polite"} - className={"wails-no-draggable flex h-10 select-none items-center gap-2 rounded-lg px-3"} + className={"wails-no-draggable flex h-10 items-center gap-2 rounded-lg px-3 select-none"} >
    -
    +
    ); @@ -272,7 +276,7 @@ const ProfileTriggerButton = forwardRef { onSelect={() => onSelect(profile.id)} className={cn( "flex w-auto gap-2 px-2 py-2 pe-3 last:mb-1", - "cursor-default rounded-md text-sm outline-none", + "cursor-default rounded-md text-sm outline-hidden", "data-[selected=true]:bg-nb-gray-900", showEmail ? "items-start" : "items-center", )} diff --git a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx index 1be2f6a44..2d0fd180b 100644 --- a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -205,7 +205,7 @@ export function ProfilesTab() {

    {t("settings.profiles.emptyTitle")}

    -

    +

    {t("settings.profiles.emptyDescription")}

    @@ -429,10 +429,10 @@ const ProfileRow = ({ className={cn( "flex items-center gap-4 px-4 py-2.5", "border-b border-nb-gray-910 last:border-b-0", - "outline-none", + "outline-hidden", isFirst && "rounded-t-xl", isLast && "rounded-b-xl", - "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-nb-gray-50/60", + "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-inset", )} > {profile.name} @@ -490,7 +490,7 @@ const TruncatedEmail = ({ email }: { email: string }) => { const span = ( {email} @@ -578,7 +578,7 @@ const RowMoreMenu = ({ aria-label={moreLabel} tabIndex={rowFocused ? 0 : -1} className={cn( - "inline-flex h-9 w-9 cursor-default items-center justify-center rounded-md outline-none", + "inline-flex h-9 w-9 cursor-default items-center justify-center rounded-md outline-hidden", "text-nb-gray-400 hover:bg-nb-gray-900 hover:text-nb-gray-100", "transition-colors duration-150", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", @@ -625,7 +625,7 @@ const DeleteMenuItem = ({ disabled, disabledReason, onDelete }: DeleteMenuItemPr
    @@ -673,7 +673,7 @@ const ActionIconButton = ({ aria-disabled={disabled || undefined} tabIndex={hidden || !tabbable ? -1 : 0} className={cn( - "inline-flex h-9 w-9 cursor-default items-center justify-center rounded-md outline-none", + "inline-flex h-9 w-9 cursor-default items-center justify-center rounded-md outline-hidden", "transition-colors duration-150", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", variant === "danger" @@ -681,7 +681,7 @@ const ActionIconButton = ({ : "text-nb-gray-400 hover:bg-nb-gray-900 hover:text-nb-gray-100", hidden && "pointer-events-none opacity-0", disabled && - "cursor-not-allowed opacity-40 hover:!bg-transparent hover:!text-nb-gray-400", + "cursor-not-allowed opacity-40 hover:bg-transparent! hover:text-nb-gray-400!", )} > diff --git a/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx b/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx index f1c67118c..e07ddf086 100644 --- a/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx +++ b/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx @@ -212,7 +212,7 @@ export default function SessionExpirationDialog() { {!expired && (
    diff --git a/client/ui/frontend/src/modules/settings/SettingsAbout.tsx b/client/ui/frontend/src/modules/settings/SettingsAbout.tsx index 83f1f81b7..11109b746 100644 --- a/client/ui/frontend/src/modules/settings/SettingsAbout.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsAbout.tsx @@ -106,7 +106,7 @@ export function SettingsAbout() { type={"button"} onClick={handleVersionClick} className={ - "cursor-text select-text bg-transparent text-sm font-semibold text-nb-gray-100 outline-none" + "cursor-text bg-transparent text-sm font-semibold text-nb-gray-100 outline-hidden select-text" } > {daemonVersion === "development" ? ( @@ -120,7 +120,7 @@ export function SettingsAbout() { t("settings.about.client", { version: daemonVersion }) )} -

    +

    {guiVersion === "development" ? ( {t("settings.about.guiName")}{" "} @@ -149,7 +149,7 @@ export function SettingsAbout() { tabIndex={0} onClick={() => openUrl(url)} className={ - "inline-flex items-center gap-1.5 rounded-sm decoration-[0.5px] underline-offset-4 outline-none transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940" + "inline-flex items-center gap-1.5 rounded-xs decoration-[0.5px] underline-offset-4 outline-hidden transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940" } > @@ -167,7 +167,7 @@ export function SettingsAbout() { tabIndex={0} onClick={() => openUrl(link.url)} className={ - "rounded-sm decoration-[0.5px] underline-offset-4 outline-none transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940" + "rounded-xs decoration-[0.5px] underline-offset-4 outline-hidden transition hover:text-nb-gray-100 hover:underline focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940" } > {link.label} diff --git a/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx b/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx index 816dcb71f..50b013273 100644 --- a/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx @@ -28,7 +28,7 @@ export const SettingsNavigation = () => { ) : undefined; return ( -

    +
    { diff --git a/client/ui/frontend/src/modules/settings/SettingsSSH.tsx b/client/ui/frontend/src/modules/settings/SettingsSSH.tsx index d74afae73..95ef4db86 100644 --- a/client/ui/frontend/src/modules/settings/SettingsSSH.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsSSH.tsx @@ -261,7 +261,7 @@ function GuardedHint({ {t("settings.ssh.privilege.hint", { actor })} - + {command} diff --git a/client/ui/frontend/src/modules/settings/SettingsSection.tsx b/client/ui/frontend/src/modules/settings/SettingsSection.tsx index 3a39d661f..86650bcb9 100644 --- a/client/ui/frontend/src/modules/settings/SettingsSection.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsSection.tsx @@ -15,12 +15,12 @@ export const SectionGroup = ({ tabIndex={disabled ? -1 : 0} {...(disabled ? { inert: "" } : {})} className={cn( - "mb-8 rounded-md px-1 outline-none last:mb-1", + "mb-8 rounded-md px-1 outline-hidden last:mb-1", "focus-visible:ring-2 focus-visible:ring-nb-gray-50/60 focus-visible:ring-offset-2 focus-visible:ring-offset-nb-gray-940", disabled && "pointer-events-none opacity-30", )} > -

    +

    {title}

    {children}
    @@ -30,7 +30,7 @@ export const SectionGroup = ({ export const SettingsBottomBar = ({ children }: { children: ReactNode }) => ( <>
    -
    +
    +
    {t("settings.troubleshooting.anonymize.info")}
    } @@ -213,7 +213,7 @@ function ProgressSection({ {stage.kind === "capturing" && (
    diff --git a/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx b/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx index 8838e5c92..7dab2a5e5 100644 --- a/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx +++ b/client/ui/frontend/src/modules/welcome/WelcomeStepTray.tsx @@ -32,7 +32,7 @@ export function WelcomeStepTray({ onContinue }: Readonly) {""}
    diff --git a/client/ui/frontend/tailwind.config.ts b/client/ui/frontend/tailwind.config.ts deleted file mode 100644 index da41ee111..000000000 --- a/client/ui/frontend/tailwind.config.ts +++ /dev/null @@ -1,180 +0,0 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - content: ["./index.html", "./src/**/*.{ts,tsx}"], - darkMode: "class", - theme: { - fontFamily: { - sans: ['"Inter Variable"', 'ui-sans-serif', 'system-ui', 'sans-serif'], - mono: ['"JetBrains Mono Variable"', 'ui-monospace', 'monospace'], - }, - extend: { - colors: { - // nb-gray is theme-aware: channels come from CSS variables in - // globals.css (:root = light ramp, .dark = original dark ramp). The - // rgb(var(...) / ) form keeps opacity modifiers working. - "nb-gray": { - DEFAULT: "rgb(var(--nb-gray-DEFAULT) / )", - 50: "rgb(var(--nb-gray-50) / )", - 100: "rgb(var(--nb-gray-100) / )", - 200: "rgb(var(--nb-gray-200) / )", - 250: "rgb(var(--nb-gray-250) / )", - 300: "rgb(var(--nb-gray-300) / )", - 350: "rgb(var(--nb-gray-350) / )", - 400: "rgb(var(--nb-gray-400) / )", - 500: "rgb(var(--nb-gray-500) / )", - 600: "rgb(var(--nb-gray-600) / )", - 700: "rgb(var(--nb-gray-700) / )", - 800: "rgb(var(--nb-gray-800) / )", - 850: "rgb(var(--nb-gray-850) / )", - 900: "rgb(var(--nb-gray-900) / )", - 910: "rgb(var(--nb-gray-910) / )", - 920: "rgb(var(--nb-gray-920) / )", - 925: "rgb(var(--nb-gray-925) / )", - 930: "rgb(var(--nb-gray-930) / )", - 935: "rgb(var(--nb-gray-935) / )", - 940: "rgb(var(--nb-gray-940) / )", - 950: "rgb(var(--nb-gray-950) / )", - 960: "rgb(var(--nb-gray-960) / )", - }, - gray: { - 50: "#F9FAFB", - 100: "#F3F4F6", - 200: "#E5E7EB", - 300: "#D1D5DB", - 400: "#9CA3AF", - 500: "#6B7280", - 600: "#4B5563", - 700: "#374151", - 800: "#1F2937", - 900: "#111827", - }, - red: { - 50: "#FDF2F2", - 100: "#FDE8E8", - 200: "#FBD5D5", - 300: "#F8B4B4", - 400: "#F98080", - 500: "#F05252", - 600: "#E02424", - 700: "#C81E1E", - 800: "#9B1C1C", - 900: "#771D1D", - }, - yellow: { - 50: "#FDFDEA", - 100: "#FDF6B2", - 200: "#FCE96A", - 300: "#FACA15", - 400: "#E3A008", - 500: "#C27803", - 600: "#9F580A", - 700: "#8E4B10", - 800: "#723B13", - 900: "#633112", - }, - green: { - 50: "#F3FAF7", - 100: "#DEF7EC", - 200: "#BCF0DA", - 300: "#84E1BC", - 400: "#31C48D", - 500: "#0E9F6E", - 600: "#057A55", - 700: "#046C4E", - 800: "#03543F", - 900: "#014737", - }, - blue: { - 50: "#EBF5FF", - 100: "#E1EFFE", - 200: "#C3DDFD", - 300: "#A4CAFE", - 400: "#76A9FA", - 500: "#3F83F8", - 600: "#1C64F2", - 700: "#1A56DB", - 800: "#1E429F", - 900: "#233876", - }, - indigo: { - 50: "#F0F5FF", - 100: "#E5EDFF", - 200: "#CDDBFE", - 300: "#B4C6FC", - 400: "#8DA2FB", - 500: "#6875F5", - 600: "#5850EC", - 700: "#5145CD", - 800: "#42389D", - 900: "#362F78", - }, - purple: { - 50: "#F6F5FF", - 100: "#EDEBFE", - 200: "#DCD7FE", - 300: "#CABFFD", - 400: "#AC94FA", - 500: "#9061F9", - 600: "#7E3AF2", - 700: "#6C2BD9", - 800: "#5521B5", - 900: "#4A1D96", - }, - pink: { - 50: "#FDF2F8", - 100: "#FCE8F3", - 200: "#FAD1E8", - 300: "#F8B4D9", - 400: "#F17EB8", - 500: "#E74694", - 600: "#D61F69", - 700: "#BF125D", - 800: "#99154B", - 900: "#751A3D", - }, - netbird: { - DEFAULT: "#f68330", - 50: "#fff6ed", - 100: "#feecd6", - 150: "#ffdfb8", - 200: "#ffd4a6", - 300: "#fab677", - 400: "#f68330", - 500: "#f46d1b", - 600: "#e55311", - 700: "#be3e10", - 800: "#973215", - 900: "#7a2b14", - 950: "#421308", - }, - }, - backgroundImage: { - "conic-netbird": "conic-gradient(from 0deg, #e55311 0%, #f68330 10%, #e55311 20%, #e55311 100%)", - }, - keyframes: { - "pulse-reverse": { - "0%, 100%": { opacity: "1" }, - "50%": { opacity: "0.4" }, - }, - "spin-slow": { - "0%": { transform: "rotate(0deg)" }, - "100%": { transform: "rotate(360deg)" }, - }, - "ping-slow": { - "0%": { transform: "scale(1)", opacity: "1" }, - "75%, 100%": { transform: "scale(2)", opacity: "0" }, - }, - }, - animation: { - "ping-slow": "ping-slow 2s cubic-bezier(0, 0, 0.2, 1) infinite", - "pulse-slow": "pulse-reverse 2s cubic-bezier(0.5, 0, 0.6, 1) infinite", - "pulse-slower": "pulse-reverse 3s cubic-bezier(0.5, 0, 0.6, 1) infinite", - "spin-slow": "spin-slow 2s linear infinite", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -}; - -export default config; diff --git a/client/ui/frontend/vite.config.ts b/client/ui/frontend/vite.config.ts index 48b1c5630..8804216a7 100644 --- a/client/ui/frontend/vite.config.ts +++ b/client/ui/frontend/vite.config.ts @@ -1,6 +1,7 @@ import path from "path"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; import wails from "@wailsio/runtime/plugins/vite"; // https://vitejs.dev/config/ @@ -14,7 +15,7 @@ export default defineConfig({ ), }, }, - plugins: [react(), wails("./bindings")], + plugins: [react(), tailwindcss(), wails("./bindings")], server: { host: "127.0.0.1", port: 9245, diff --git a/combined/cmd/admin.go b/combined/cmd/admin.go index 66fac4ac9..15dc15130 100644 --- a/combined/cmd/admin.go +++ b/combined/cmd/admin.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" diff --git a/go.mod b/go.mod index beb612a72..f93141d5c 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 - github.com/kardianos/service v1.2.3-0.20240613133416-becf2eb62b83 + github.com/kardianos/service v1.3.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.34.1 github.com/rs/cors v1.8.0 @@ -23,7 +23,6 @@ require ( github.com/vishvananda/netlink v1.3.1 golang.org/x/crypto v0.55.0 golang.org/x/sys v0.47.0 - golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/grpc v1.80.0 @@ -48,8 +47,6 @@ require ( github.com/creack/pty v1.1.24 github.com/crowdsecurity/crowdsec v1.7.7 github.com/crowdsecurity/go-cs-bouncer v0.0.21 - github.com/dexidp/dex v2.13.0+incompatible - github.com/dexidp/dex/api/v2 v2.4.0 github.com/docker/docker v28.0.1+incompatible github.com/docker/go-connections v0.6.0 github.com/ebitengine/purego v0.9.1 @@ -85,16 +82,19 @@ require ( github.com/miekg/dns v1.1.72 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/moby/moby/api v1.54.1 + github.com/netbirdio/dex v0.244.1-0.20261021152327-0319b501f1a6 + github.com/netbirdio/dex/api/v2 v2.0.0-20261021115841-9f3e6d4c2ce3 github.com/netbirdio/go-nat v0.0.0-20260821095157-6b2c8c5c74e8 - github.com/netbirdio/management-integrations/integrations v0.0.0-20261006132740-6e6b0cb01f2e + github.com/netbirdio/ice/v4 v4.0.0-20261021152109-d0e5dfacb548 + github.com/netbirdio/management-integrations/integrations v0.0.0-20261021190039-6c1585eb11de github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 + github.com/netbirdio/wireguard-go v0.0.0-20261021152105-0de46b309cdb github.com/oapi-codegen/runtime v1.1.2 github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/ory/dockertest/v4 v4.0.0 github.com/oschwald/maxminddb-golang v1.12.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 - github.com/pion/ice/v4 v4.0.0-00010101000000-000000000000 github.com/pion/logging v0.2.4 github.com/pion/randutil v0.1.0 github.com/pion/stun/v2 v2.0.0 @@ -148,6 +148,8 @@ require ( howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 ) +require golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect + require ( cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -363,20 +365,8 @@ require ( rsc.io/qr v0.2.0 // indirect ) -replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 - -replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 - -replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20261016113635-bd0cb75a3df0 - replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 -replace github.com/pion/ice/v4 => github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 - -replace github.com/dexidp/dex => github.com/netbirdio/dex v0.244.1-0.20260716205454-a163de3129e5 - -replace github.com/dexidp/dex/api/v2 => github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1 - replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0 replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20261009120932-4e087a8bfd6d diff --git a/go.sum b/go.sum index 82e2daf78..a6d3dedca 100644 --- a/go.sum +++ b/go.sum @@ -424,6 +424,8 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kardianos/service v1.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI= +github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= @@ -522,26 +524,24 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/netbirdio/dex v0.244.1-0.20260716205454-a163de3129e5 h1:3PwQv8aR46qN2u16+Dv6udnH3sbVKX5KrGwF35CKSI0= -github.com/netbirdio/dex v0.244.1-0.20260716205454-a163de3129e5/go.mod h1:IHH+H8vK2GfqtIt5u/5OdPh18yk0oDHuj2vz5+Goetg= -github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1 h1:neE7z+FPUkldl3faK/Jt+hJK2L+1XfQ1W33TQhU9m88= -github.com/netbirdio/dex/api/v2 v2.0.0-20260512110716-8d70ad8647c1/go.mod h1:awuTyT29CYALpEyET0S307EgNlPWrc7fFKRAyhsO45M= +github.com/netbirdio/dex v0.244.1-0.20261021152327-0319b501f1a6 h1:DVyt4qYA79/4eVdw7BNKDCmR9wti2YT3iHvIdPiV4UI= +github.com/netbirdio/dex v0.244.1-0.20261021152327-0319b501f1a6/go.mod h1:98h6OBd3fEU5lyQ5dhLWtSna5IUGjBz5awC0NmtS6s4= +github.com/netbirdio/dex/api/v2 v2.0.0-20261021115841-9f3e6d4c2ce3 h1:5yuaucSBsXe/wcXTdjo2UKmY352xK3ESWzbwfOyuQvw= +github.com/netbirdio/dex/api/v2 v2.0.0-20261021115841-9f3e6d4c2ce3/go.mod h1:SnT8viXNefO9j0z9gdfZ8vk7t1TJ2CmbJZkIL4fuDus= github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6Aus= github.com/netbirdio/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/netbirdio/go-nat v0.0.0-20260821095157-6b2c8c5c74e8 h1:pBxXEsxcsO3qVUND//5j1kelYlO57x5IrRviNF0+0iA= github.com/netbirdio/go-nat v0.0.0-20260821095157-6b2c8c5c74e8/go.mod h1:mFViabv4PpnoDw9w7W21a7xux6APA4q7KQZRsv4BCl8= -github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI= -github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8= -github.com/netbirdio/management-integrations/integrations v0.0.0-20261006132740-6e6b0cb01f2e h1:SnDCreUnY+QDxKPueUoNrniMALCu2VFkXTCr/RIZjdg= -github.com/netbirdio/management-integrations/integrations v0.0.0-20261006132740-6e6b0cb01f2e/go.mod h1:n47r67ZSPgwSmT/Z1o48JjZQW9YJ6m/6Bd/uAXkL3Pg= -github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8= -github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/netbirdio/ice/v4 v4.0.0-20261021152109-d0e5dfacb548 h1:dMRtLZZm7qpyfuw2uRKmCYySYz51wihEWY3JUYMpuK4= +github.com/netbirdio/ice/v4 v4.0.0-20261021152109-d0e5dfacb548/go.mod h1:UsaaFHJPfIMNzvOybklN9AO3v/4YGj6bCC1vpmYA0vo= +github.com/netbirdio/management-integrations/integrations v0.0.0-20261021190039-6c1585eb11de h1:aCmxkLAjH3pF/88eUAXp9yMOwGtcvNTC5Mqszm9xuo0= +github.com/netbirdio/management-integrations/integrations v0.0.0-20261021190039-6c1585eb11de/go.mod h1:LLvpw/iWbXX9+7HF7ueqkyE7SoFaeqoCmbskOQL9gbs= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20261009120932-4e087a8bfd6d h1:DV0FHFhNAaU69JHXKclYQWr/ZJoc2BA/ZpqlnX8l22U= github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20261009120932-4e087a8bfd6d/go.mod h1:62UsqQRqanuCbFi6xElIw3/2X+yGF+TKlyiWImA5taU= -github.com/netbirdio/wireguard-go v0.0.0-20261016113635-bd0cb75a3df0 h1:nCK1YzDybjbAfrx+C2QPmcQ+7jYj4Rcb3FskqTNZEXo= -github.com/netbirdio/wireguard-go v0.0.0-20261016113635-bd0cb75a3df0/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= +github.com/netbirdio/wireguard-go v0.0.0-20261021152105-0de46b309cdb h1:3X7pVeZeneq4MK7hvvqeSGi1ir6OElgIqHd6cI4KsXo= +github.com/netbirdio/wireguard-go v0.0.0-20261021152105-0de46b309cdb/go.mod h1:Vk/c438m8FV2hHcT/6Efp8xe/JhHTzf3eUcuaohKKkw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= @@ -889,7 +889,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -969,6 +968,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= +golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= diff --git a/idp/dex/config.go b/idp/dex/config.go index 00b5ce745..009426602 100644 --- a/idp/dex/config.go +++ b/idp/dex/config.go @@ -14,9 +14,9 @@ import ( "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v3" - "github.com/dexidp/dex/server" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/sql" + "github.com/netbirdio/dex/server" + "github.com/netbirdio/dex/storage" + "github.com/netbirdio/dex/storage/sql" "github.com/netbirdio/netbird/idp/dex/web" ) diff --git a/idp/dex/connector.go b/idp/dex/connector.go index fb20fdcc3..a4653cc48 100644 --- a/idp/dex/connector.go +++ b/idp/dex/connector.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" ) // ConnectorConfig represents the configuration for an identity provider connector diff --git a/idp/dex/connector_test.go b/idp/dex/connector_test.go index 4253e02b7..780cfebcb 100644 --- a/idp/dex/connector_test.go +++ b/idp/dex/connector_test.go @@ -8,8 +8,8 @@ import ( "path/filepath" "testing" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/sql" + "github.com/netbirdio/dex/storage" + "github.com/netbirdio/dex/storage/sql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/idp/dex/provider.go b/idp/dex/provider.go index 95c511c01..8062dd00b 100644 --- a/idp/dex/provider.go +++ b/idp/dex/provider.go @@ -16,12 +16,12 @@ import ( "sync" "time" - dexapi "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/server" - "github.com/dexidp/dex/server/signer" - "github.com/dexidp/dex/storage" "github.com/go-jose/go-jose/v4" "github.com/google/uuid" + dexapi "github.com/netbirdio/dex/api/v2" + "github.com/netbirdio/dex/server" + "github.com/netbirdio/dex/server/signer" + "github.com/netbirdio/dex/storage" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/bcrypt" "google.golang.org/grpc" diff --git a/idp/dex/provider_test.go b/idp/dex/provider_test.go index 5e132d544..5ea1260a8 100644 --- a/idp/dex/provider_test.go +++ b/idp/dex/provider_test.go @@ -12,9 +12,9 @@ import ( "path/filepath" "testing" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" - sqllib "github.com/dexidp/dex/storage/sql" + "github.com/netbirdio/dex/storage" + "github.com/netbirdio/dex/storage/memory" + sqllib "github.com/netbirdio/dex/storage/sql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/idp/dex/sqlite_cgo.go b/idp/dex/sqlite_cgo.go index 5de66f647..0339b48fd 100644 --- a/idp/dex/sqlite_cgo.go +++ b/idp/dex/sqlite_cgo.go @@ -3,7 +3,7 @@ package dex import ( - sql "github.com/dexidp/dex/storage/sql" + sql "github.com/netbirdio/dex/storage/sql" ) // newSQLite3 builds the dex SQLite3 config. CGO builds use the upstream diff --git a/idp/dex/sqlite_nocgo.go b/idp/dex/sqlite_nocgo.go index 4def12143..f67fa64e3 100644 --- a/idp/dex/sqlite_nocgo.go +++ b/idp/dex/sqlite_nocgo.go @@ -3,7 +3,7 @@ package dex import ( - sql "github.com/dexidp/dex/storage/sql" + sql "github.com/netbirdio/dex/storage/sql" ) // newSQLite3 for non-CGO builds. The dex SQLite3 stub has no fields and its diff --git a/idp/sdk/sdk.go b/idp/sdk/sdk.go index d2189135b..6a5d3d49c 100644 --- a/idp/sdk/sdk.go +++ b/idp/sdk/sdk.go @@ -4,7 +4,7 @@ package sdk import ( "context" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" "github.com/netbirdio/netbird/idp/dex" ) diff --git a/management/cmd/admin.go b/management/cmd/admin.go index e5c0f6ac9..14591fa99 100644 --- a/management/cmd/admin.go +++ b/management/cmd/admin.go @@ -6,7 +6,7 @@ import ( "path" "path/filepath" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" diff --git a/management/cmd/admin/admin.go b/management/cmd/admin/admin.go index bd56af39b..702725fd5 100644 --- a/management/cmd/admin/admin.go +++ b/management/cmd/admin/admin.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/crypto/bcrypt" diff --git a/management/cmd/admin/admin_test.go b/management/cmd/admin/admin_test.go index dd1b8ed06..07e0f10da 100644 --- a/management/cmd/admin/admin_test.go +++ b/management/cmd/admin/admin_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" + "github.com/netbirdio/dex/storage" + "github.com/netbirdio/dex/storage/memory" "github.com/spf13/cobra" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index 9b8471c1b..08d018e5f 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -933,7 +933,7 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi } // it's possible that the peer gets deleted between the call to "sendInitialSync()" and here, bail out in this case - if _, ok := account.Peers[peer.ID]; !ok { + if p, _ := account.Peers[peer.ID]; p == nil { return nil, nil, nil, 0, fmt.Errorf("peer '%s' no longer exists", peer.ID) } diff --git a/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go b/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go index 6705eab1a..a0b6c60df 100644 --- a/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go +++ b/management/internals/modules/reverseproxy/accesslogs/accesslogentry.go @@ -22,9 +22,9 @@ const ( type AccessLogEntry struct { ID string `gorm:"primaryKey"` - AccountID string `gorm:"index"` + AccountID string `gorm:"index;index:idx_access_log_entries_account_timestamp,priority:1,option:CONCURRENTLY"` ServiceID string `gorm:"index"` - Timestamp time.Time `gorm:"index"` + Timestamp time.Time `gorm:"index;index:idx_access_log_entries_account_timestamp,priority:2"` GeoLocation peer.Location `gorm:"embedded;embeddedPrefix:location_"` SubdivisionCode string Method string `gorm:"index"` diff --git a/management/internals/network_map_db/pgsql/group.go b/management/internals/network_map_db/pgsql/group.go index 874e743a5..a52b4abea 100644 --- a/management/internals/network_map_db/pgsql/group.go +++ b/management/internals/network_map_db/pgsql/group.go @@ -17,7 +17,7 @@ const ( ( select array_agg(group_peers.peer_id) from group_peers - where group_peers.group_id = groups.id and group_peers.account_id=$1 + where group_peers.group_id = groups.id ) as peers from groups where account_id=$1 ` diff --git a/management/internals/shared/grpc/components_encoder.go b/management/internals/shared/grpc/components_encoder.go index a4516be52..4763177ab 100644 --- a/management/internals/shared/grpc/components_encoder.go +++ b/management/internals/shared/grpc/components_encoder.go @@ -20,6 +20,7 @@ const wgKeyRawLen = 32 // computed alongside the components in the network_map controller and reused // from the legacy proto path) and the dns_domain string. type ComponentsEnvelopeInput struct { + Peer *nmdata.Peer // only used when encoding "empty" component maps Components *types.NetworkMapComponents PeerConfig *proto.PeerConfig DNSDomain string @@ -59,8 +60,8 @@ func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvel Serial: networkSerial(c.Network), Network: toAccountNetwork(c.Network), PeerConfig: in.PeerConfig, - // components.Peers always contains the target peer - Peers: []*proto.PeerCompact{toPeerCompact(c.Peers[c.PeerID])}, + // components.Peers may not contain the target peer + Peers: []*proto.PeerCompact{toPeerCompact(in.Peer)}, DnsDomain: in.DNSDomain, DnsForwarderPort: in.DNSForwarderPort, UserIdClaim: in.UserIDClaim, diff --git a/management/internals/shared/grpc/components_encoder_test.go b/management/internals/shared/grpc/components_encoder_test.go index a6421af11..a9e9cd939 100644 --- a/management/internals/shared/grpc/components_encoder_test.go +++ b/management/internals/shared/grpc/components_encoder_test.go @@ -717,6 +717,7 @@ func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) { // nil Components → minimal envelope, no crash. Matches the legacy // behaviour for missing/unvalidated peers. env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{ + Peer: &nmdata.Peer{ID: "missing-peer", SSHKey: "missing-peer-key"}, Components: emptyNetworkMapComponents(), DNSDomain: "netbird.cloud", }) @@ -731,6 +732,8 @@ func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) { require.NotNil(t, full.Network, "client runs Calculate() over the envelope and dereferences Network unconditionally; a nil here would crash the receiver") assert.Equal(t, "net-empty", full.Network.Identifier) assert.Equal(t, uint64(9), full.Serial) + assert.Len(t, env.GetFull().Peers, 1) + assert.Equal(t, string(env.GetFull().Peers[0].SshPubKey), "missing-peer-key") } func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) { diff --git a/management/internals/shared/grpc/components_envelope_response.go b/management/internals/shared/grpc/components_envelope_response.go index cbf9bb477..40ac7a0c7 100644 --- a/management/internals/shared/grpc/components_envelope_response.go +++ b/management/internals/shared/grpc/components_envelope_response.go @@ -55,6 +55,7 @@ func ToComponentSyncResponse( } envelope := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{ + Peer: peer, Components: components, PeerConfig: peerConfig, DNSDomain: dnsName, diff --git a/management/server/account.go b/management/server/account.go index 68150a947..ddc8d4bbc 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -298,6 +298,9 @@ func BuildManager( am.integratedPeerValidator.SetPeerInvalidationListener(func(accountID string, peerIDs []string) { am.onPeersInvalidated(ctx, accountID, peerIDs) }) + am.integratedPeerValidator.SetPeerValidationChangeListener(func(accountID string, peerIDs []string) { + am.onPeerValidationChanged(ctx, accountID, peerIDs) + }) // Started last: a manager that fails to build is never returned, so nothing would // stop its refresher. @@ -806,6 +809,21 @@ func (am *DefaultAccountManager) schedulePeerLoginExpiration(ctx context.Context } } +// schedulePeerInactivityExpiration arms the account's inactivity expiration job for a +// connecting peer unless one is already scheduled: a connected peer cannot expire, so it +// never moves the scheduled run earlier. Disconnects must not skip a running job, which may +// be finishing its last run without seeing the newly disconnected peer. +func (am *DefaultAccountManager) schedulePeerInactivityExpiration(ctx context.Context, accountID string) { + if am.peerInactivityExpiry.IsSchedulerRunning(accountID) { + log.WithContext(ctx).Tracef("peer inactivity expiration job for account %s is already scheduled", accountID) + return + } + jobCtx := context.WithoutCancel(ctx) + if nextRun, ok := am.getNextInactivePeerExpiration(ctx, accountID); ok { + go am.peerInactivityExpiry.Schedule(jobCtx, nextRun, accountID, am.peerInactivityExpirationJob(jobCtx, accountID)) + } +} + // peerInactivityExpirationJob marks login expired for all inactive peers and returns the minimum duration in which the next peer of the account will expire by inactivity if found func (am *DefaultAccountManager) peerInactivityExpirationJob(ctx context.Context, accountID string) func() (time.Duration, bool) { return func() (time.Duration, bool) { @@ -2087,6 +2105,22 @@ func (am *DefaultAccountManager) onPeersInvalidated(ctx context.Context, account log.WithContext(ctx).Debugf("invalidated peers have been expired for account %s", accountID) } +// onPeerValidationChanged pushes fresh network maps to peers whose validation state +// changed in the background, and to the peers that can reach them. +func (am *DefaultAccountManager) onPeerValidationChanged(ctx context.Context, accountID string, peerIDs []string) { + if len(peerIDs) == 0 { + return + } + affected := am.resolveAffectedPeersForPeerChanges(ctx, am.Store, accountID, peerIDs) + if len(affected) == 0 { + // Resolution failed or found nothing; the changed peers' own maps still differ. + affected = peerIDs + } + if err := am.networkMapController.OnPeersUpdated(ctx, accountID, peerIDs, affected); err != nil { + log.WithContext(ctx).Errorf("failed to update peers after validation change for account %s: %v", accountID, err) + } +} + func (am *DefaultAccountManager) FindExistingPostureCheck(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error) { return am.Store.GetPostureCheckByChecksDefinition(accountID, checks) } diff --git a/management/server/account_test.go b/management/server/account_test.go index 8f7b1f18b..7acc72d1f 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "testing" "testing/synctest" "time" @@ -50,6 +51,7 @@ import ( "github.com/netbirdio/netbird/management/server/cache" "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" "github.com/netbirdio/netbird/management/server/idp" + "github.com/netbirdio/netbird/management/server/instance" "github.com/netbirdio/netbird/management/server/job" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" @@ -844,6 +846,49 @@ func TestAccountManager_GetAccountByUserID(t *testing.T) { } } +type setupOwnerInstanceManager struct { + instance.Manager + user *idp.UserData +} + +func (m setupOwnerInstanceManager) CreateOwnerUser(context.Context, string, string, string) (*idp.UserData, error) { + return m.user, nil +} + +// The /api/setup owner account must get the single account mode domain, so later +// users join that account instead of forking a new one. +func TestSetupOwner_SingleAccountMode_LaterUsersJoinSetupAccount(t *testing.T) { + t.Setenv(instance.SetupPATEnabledEnvKey, "true") + + manager, _, err := createManager(t) + require.NoError(t, err) + manager.singleAccountMode = true + manager.singleAccountModeDomain = "netbird.selfhosted" + + ctx := context.Background() + owner := &idp.UserData{ID: "owner", Email: "owner@example.com", Name: "Owner"} + setup := instance.NewSetupService(setupOwnerInstanceManager{user: owner}, manager) + _, err = setup.SetupOwner(ctx, owner.Email, "password", owner.Name, instance.SetupOptions{CreatePAT: true}) + require.NoError(t, err) + + ownerAccountID, err := manager.Store.GetAccountIDByUserID(ctx, store.LockingStrengthNone, owner.ID) + require.NoError(t, err) + acc, err := manager.Store.GetAccount(ctx, ownerAccountID) + require.NoError(t, err) + assert.Equal(t, "netbird.selfhosted", acc.Domain) + assert.Equal(t, types.PrivateCategory, acc.DomainCategory) + assert.True(t, acc.IsDomainPrimaryAccount) + + jitAccountID, _, err := manager.GetAccountIDFromUserAuth(ctx, auth.UserAuth{ + UserId: "jit-user", + Email: "jit@gmail.com", + Domain: "gmail.com", + DomainCategory: types.PublicCategory, + }) + require.NoError(t, err) + assert.Equal(t, ownerAccountID, jitAccountID) +} + func createAccount(am *DefaultAccountManager, accountID, userID, domain string) (*types.Account, error) { account := newAccountWithId(context.Background(), accountID, userID, domain, "", "", false) err := am.Store.SaveAccount(context.Background(), account) @@ -2233,6 +2278,57 @@ func TestDefaultAccountManager_MarkPeerDisconnected_SchedulesInactivityExpiratio } } +func TestDefaultAccountManager_PeerConnect_KeepsScheduledInactivityExpiration(t *testing.T) { + manager, _, err := createManager(t) + require.NoError(t, err, "unable to create account manager") + + accountID, err := manager.GetAccountIDByUserID(context.Background(), auth.UserAuth{UserId: userID}) + require.NoError(t, err, "unable to create an account") + + key, err := wgtypes.GenerateKey() + require.NoError(t, err, "unable to generate WireGuard key") + peerPubKey := key.PublicKey().String() + + _, _, _, _, err = manager.AddPeer(context.Background(), "", "", userID, &nbpeer.Peer{ + Key: peerPubKey, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"}, + InactivityExpirationEnabled: true, + }, false) + require.NoError(t, err, "unable to add peer") + + _, err = manager.UpdateAccountSettings(context.Background(), accountID, userID, &types.Settings{ + PeerLoginExpiration: time.Hour, + PeerLoginExpirationEnabled: true, + PeerInactivityExpiration: time.Hour, + PeerInactivityExpirationEnabled: true, + Extra: &types.ExtraSettings{}, + }) + require.NoError(t, err, "expecting to update account settings successfully but got error") + + // The account already has an inactivity job scheduled. A connecting peer + // cannot expire, so connect must leave that job alone instead of cancelling + // it and re-scanning every peer of the account. Cancel runs synchronously, + // so counting it observes the decision deterministically. + var cancels atomic.Int32 + manager.peerInactivityExpiry = &MockScheduler{ + IsSchedulerRunningFunc: func(ID string) bool { return ID == accountID }, + CancelFunc: func(ctx context.Context, IDs []string) { cancels.Add(1) }, + ScheduleFunc: func(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) { + }, + } + + streamStartTime := time.Now().UTC() + err = manager.MarkPeerConnected(context.Background(), peerPubKey, accountID, streamStartTime.UnixNano(), nil) + require.NoError(t, err, "unable to mark peer connected") + assert.Zero(t, cancels.Load(), "connect must not cancel a scheduled inactivity job") + + // A disconnect still cancels and re-arms: the running job may be finishing + // its last run without having seen this peer as disconnected. + err = manager.MarkPeerDisconnected(context.Background(), peerPubKey, accountID, streamStartTime.UnixNano()) + require.NoError(t, err, "unable to mark peer disconnected") + assert.Equal(t, int32(1), cancels.Load(), "disconnect must cancel and re-arm the inactivity job") +} + func TestDefaultAccountManager_MarkPeerDisconnected_SkipsInactivityExpirationWhenDisabled(t *testing.T) { manager, _, err := createManager(t) require.NoError(t, err, "unable to create account manager") diff --git a/management/server/activity/event.go b/management/server/activity/event.go index 8fd5e3371..2d66418a3 100644 --- a/management/server/activity/event.go +++ b/management/server/activity/event.go @@ -17,7 +17,7 @@ type ActivityDescriber interface { //nolint:revive // Event represents a network/system activity event. type Event struct { // Timestamp of the event - Timestamp time.Time + Timestamp time.Time `gorm:"index:idx_events_account_timestamp,priority:2"` // Activity that was performed during the event Activity Activity `gorm:"type:integer"` // ID of the event (can be empty, meaning that it wasn't yet generated) @@ -31,7 +31,7 @@ type Event struct { // TargetID is the ID of an object that was effected by the event (e.g., a peer) TargetID string // AccountID is the ID of an account where the event happened - AccountID string `gorm:"index"` + AccountID string `gorm:"index;index:idx_events_account_timestamp,priority:1,option:CONCURRENTLY"` // Meta of the event, e.g. deleted peer information like name, IP, etc Meta map[string]any `gorm:"serializer:json"` diff --git a/management/server/http/handlers/instance/instance_handler_test.go b/management/server/http/handlers/instance/instance_handler_test.go index ba59497fa..8c72c112f 100644 --- a/management/server/http/handlers/instance/instance_handler_test.go +++ b/management/server/http/handlers/instance/instance_handler_test.go @@ -363,9 +363,9 @@ func TestSetup_PAT_MissingExpireIn_DefaultsToOneDay(t *testing.T) { }, } accountMgr := &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + GetAccountIDFromUserAuthFunc: func(_ context.Context, userAuth auth.UserAuth) (string, string, error) { assert.Equal(t, "u1", userAuth.UserId) - return "acc-1", nil + return "acc-1", "u1", nil }, CreatePATFunc: func(_ context.Context, accountID, initiator, target, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { assert.Equal(t, "acc-1", accountID) @@ -425,10 +425,10 @@ func TestSetup_PAT_Success(t *testing.T) { email string }{} accountMgr := &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + GetAccountIDFromUserAuthFunc: func(_ context.Context, userAuth auth.UserAuth) (string, string, error) { gotAccountArgs.userID = userAuth.UserId gotAccountArgs.email = userAuth.Email - return "acc-1", nil + return "acc-1", "u1", nil }, CreatePATFunc: func(_ context.Context, accountID, initiator, target, name string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { assert.Equal(t, "acc-1", accountID) @@ -479,8 +479,8 @@ func TestSetup_PAT_AccountCreationFails_Rollback(t *testing.T) { }, } accountMgr := &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, _ auth.UserAuth) (string, error) { - return "", errors.New("db down") + GetAccountIDFromUserAuthFunc: func(_ context.Context, _ auth.UserAuth) (string, string, error) { + return "", "", errors.New("db down") }, GetStoreFunc: func() nbstore.Store { return accountStore @@ -505,7 +505,7 @@ func TestSetup_PAT_CreatePATFails_Rollback(t *testing.T) { ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) - account := &types.Account{Id: "acc-1"} + account := &types.Account{Id: "acc-1", CreatedBy: "owner-id"} accountStore.EXPECT().GetAccount(gomock.Any(), "acc-1").Return(account, nil) accountStore.EXPECT().DeleteAccount(gomock.Any(), account).Return(nil) @@ -521,8 +521,8 @@ func TestSetup_PAT_CreatePATFails_Rollback(t *testing.T) { }, } accountMgr := &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, _ auth.UserAuth) (string, error) { - return "acc-1", nil + GetAccountIDFromUserAuthFunc: func(_ context.Context, _ auth.UserAuth) (string, string, error) { + return "acc-1", "u1", nil }, CreatePATFunc: func(_ context.Context, _, _, _, _ string, _ int) (*types.PersonalAccessTokenGenerated, error) { return nil, status.Errorf(status.Internal, "token store unavailable") diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index 7c21b6a55..88b4fcdb9 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -185,9 +185,10 @@ func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, authHeaderParts [] return fmt.Errorf("token expired") } - err = m.authManager.MarkPATUsed(ctx, pat.ID) - if err != nil { - return err + if pat.LastUsed == nil || time.Since(*pat.LastUsed) >= types.PATLastUsedUpdateInterval { + if err := m.authManager.MarkPATUsed(ctx, pat.ID); err != nil { + return err + } } userAuth := auth.UserAuth{ diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go index 9e60af1fa..127ca06d1 100644 --- a/management/server/http/middleware/auth_middleware_test.go +++ b/management/server/http/middleware/auth_middleware_test.go @@ -779,3 +779,66 @@ func TestAuthMiddleware_Handler_Child(t *testing.T) { }) } } + +func TestAuthMiddleware_MarkPATUsedThrottled(t *testing.T) { + tt := []struct { + name string + lastUsed *time.Time + expectMark bool + }{ + {name: "never used", lastUsed: nil, expectMark: true}, + {name: "used just now", lastUsed: util.ToPtr(time.Now().UTC()), expectMark: false}, + {name: "used before the update interval", lastUsed: util.ToPtr(time.Now().UTC().Add(-2 * types.PATLastUsedUpdateInterval)), expectMark: true}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + pat := &types.PersonalAccessToken{ + ID: tokenID, + ExpirationDate: util.ToPtr(time.Now().UTC().AddDate(0, 0, 7)), + LastUsed: tc.lastUsed, + } + + marked := 0 + mockAuth := &auth.MockManager{ + GetPATInfoFunc: func(_ context.Context, _ string) (*types.User, *types.PersonalAccessToken, string, string, error) { + return testAccount.Users[userID], pat, testAccount.Domain, testAccount.DomainCategory, nil + }, + MarkPATUsedFunc: func(_ context.Context, _ string) error { + marked++ + return nil + }, + } + + disabledLimiter := ratelimit.NewAPIRateLimiter(nil) + disabledLimiter.SetEnabled(false) + authMiddleware := NewAuthMiddleware( + mockAuth, + func(ctx context.Context, userAuth nbauth.UserAuth) (string, string, error) { + return userAuth.AccountId, userAuth.UserId, nil + }, + func(ctx context.Context, userAuth nbauth.UserAuth) error { + return nil + }, + func(ctx context.Context, userAuth nbauth.UserAuth) (*types.User, error) { + return &types.User{}, nil + }, + disabledLimiter, + nil, + ) + + handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + req := httptest.NewRequest("GET", "http://testing/test", nil) + req.Header.Set("Authorization", "Token "+PAT) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + if tc.expectMark { + assert.Equal(t, 1, marked, "MarkPATUsed must be called") + } else { + assert.Equal(t, 0, marked, "MarkPATUsed must be skipped") + } + }) + } +} diff --git a/management/server/identity_provider.go b/management/server/identity_provider.go index 764d8598b..a0f6ff457 100644 --- a/management/server/identity_provider.go +++ b/management/server/identity_provider.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" "github.com/rs/xid" log "github.com/sirupsen/logrus" diff --git a/management/server/idp/dex.go b/management/server/idp/dex.go index 7d25c6ed0..e9e3d4d90 100644 --- a/management/server/idp/dex.go +++ b/management/server/idp/dex.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/dexidp/dex/api/v2" + "github.com/netbirdio/dex/api/v2" log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" "google.golang.org/grpc" diff --git a/management/server/idp/embedded.go b/management/server/idp/embedded.go index b045d6ad6..8125b0449 100644 --- a/management/server/idp/embedded.go +++ b/management/server/idp/embedded.go @@ -11,8 +11,8 @@ import ( "path" "strings" - "github.com/dexidp/dex/storage" "github.com/google/uuid" + "github.com/netbirdio/dex/storage" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/idp/dex" diff --git a/management/server/instance/manager.go b/management/server/instance/manager.go index 2c355bb3b..33ab57643 100644 --- a/management/server/instance/manager.go +++ b/management/server/instance/manager.go @@ -12,8 +12,8 @@ import ( "sync" "time" - "github.com/dexidp/dex/storage" goversion "github.com/hashicorp/go-version" + "github.com/netbirdio/dex/storage" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/idp" diff --git a/management/server/instance/manager_test.go b/management/server/instance/manager_test.go index 5ffb016de..74084634a 100644 --- a/management/server/instance/manager_test.go +++ b/management/server/instance/manager_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/dexidp/dex/storage" + "github.com/netbirdio/dex/storage" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/management/server/instance/setup_service.go b/management/server/instance/setup_service.go index 92a4923be..1ef3e9736 100644 --- a/management/server/instance/setup_service.go +++ b/management/server/instance/setup_service.go @@ -10,6 +10,7 @@ import ( "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/shared/auth" "github.com/netbirdio/netbird/shared/management/status" ) @@ -111,7 +112,9 @@ func (m *SetupService) SetupOwner(ctx context.Context, email, password, name str Name: userData.Name, } - accountID, err := m.accountManager.GetAccountIDByUserID(ctx, userAuth) + // Resolve through the same path as a first login so single account mode + // stamps the configured domain, private category and primary flag. + accountID, _, err := m.accountManager.GetAccountIDFromUserAuth(ctx, userAuth) if err != nil { err = fmt.Errorf("create account for setup user: %w", err) if rollbackErr := m.rollbackSetup(ctx, userData.ID, "account provisioning failed", err, ""); rollbackErr != nil { @@ -145,7 +148,7 @@ func (m *SetupService) rollbackSetup(ctx context.Context, userID, reason string, } if accountID != "" { - if err := m.rollbackSetupAccount(ctx, accountID); err != nil { + if err := m.rollbackSetupAccount(ctx, accountID, userID); err != nil { rollbackErr := fmt.Errorf("roll back setup account %s: %w", accountID, err) log.WithContext(ctx).Errorf("failed to roll back setup account %s for user %s after %s: original error: %v, rollback error: %v", accountID, userID, reason, origErr, rollbackErr) return rollbackErr @@ -186,8 +189,9 @@ func (m *SetupService) lookupSetupAccountIDForRollback(ctx context.Context, user // rollbackSetupAccount removes only the setup-created account data from the // store. It intentionally avoids accountManager.DeleteAccount because the normal // account deletion path also deletes users from the IdP; embedded IdP cleanup is -// owned by instanceManager.RollbackSetup. -func (m *SetupService) rollbackSetupAccount(ctx context.Context, accountID string) error { +// owned by instanceManager.RollbackSetup. It keeps an account another user +// created or joined, which a concurrent first login can produce. +func (m *SetupService) rollbackSetupAccount(ctx context.Context, accountID, userID string) error { if m.accountManager == nil { return fmt.Errorf("account manager is required to roll back setup account") } @@ -205,6 +209,11 @@ func (m *SetupService) rollbackSetupAccount(ctx context.Context, accountID strin return fmt.Errorf("get setup account for rollback: %w", err) } + if !isSetupOnlyAccount(account, userID) { + log.WithContext(ctx).Warnf("keeping account %s on setup rollback: shared with another user", accountID) + return nil + } + if err := accountStore.DeleteAccount(ctx, account); err != nil { if isNotFoundError(err) { return nil @@ -214,3 +223,15 @@ func (m *SetupService) rollbackSetupAccount(ctx context.Context, accountID strin return nil } + +func isSetupOnlyAccount(account *types.Account, userID string) bool { + if account.CreatedBy != userID { + return false + } + for id := range account.Users { + if id != userID { + return false + } + } + return true +} diff --git a/management/server/instance/setup_service_test.go b/management/server/instance/setup_service_test.go index af3a91b75..4fc210f5b 100644 --- a/management/server/instance/setup_service_test.go +++ b/management/server/instance/setup_service_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "go.uber.org/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/mock_server" @@ -87,9 +87,9 @@ func TestSetupOwner_PATFeatureEnabled_MissingExpireDefaultsToOneDay(t *testing.T }, }, &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + GetAccountIDFromUserAuthFunc: func(_ context.Context, userAuth auth.UserAuth) (string, string, error) { assert.Equal(t, "owner-id", userAuth.UserId) - return "acc-1", nil + return "acc-1", "owner-id", nil }, CreatePATFunc: func(_ context.Context, accountID, initiatorUserID, targetUserID, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { assert.Equal(t, "acc-1", accountID) @@ -147,7 +147,7 @@ func TestSetupOwner_AccountProvisioningFails_RollsBackSideEffectAccountAndUser(t ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) - account := &types.Account{Id: "acc-1"} + account := &types.Account{Id: "acc-1", CreatedBy: "owner-id"} accountStore.EXPECT().GetAccountIDByUserID(gomock.Any(), nbstore.LockingStrengthNone, "owner-id").Return("acc-1", nil) accountStore.EXPECT().GetAccount(gomock.Any(), "acc-1").Return(account, nil) accountStore.EXPECT().DeleteAccount(gomock.Any(), account).Return(nil) @@ -163,9 +163,9 @@ func TestSetupOwner_AccountProvisioningFails_RollsBackSideEffectAccountAndUser(t }, }, &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + GetAccountIDFromUserAuthFunc: func(_ context.Context, userAuth auth.UserAuth) (string, string, error) { assert.Equal(t, "owner-id", userAuth.UserId) - return "", errors.New("metadata update failed") + return "", "", errors.New("metadata update failed") }, GetStoreFunc: func() nbstore.Store { return accountStore @@ -190,7 +190,7 @@ func TestSetupOwner_CreatePATFails_RollsBackSetupAccountAndUser(t *testing.T) { ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) - account := &types.Account{Id: "acc-1"} + account := &types.Account{Id: "acc-1", CreatedBy: "owner-id"} accountStore.EXPECT().GetAccount(gomock.Any(), "acc-1").Return(account, nil) accountStore.EXPECT().DeleteAccount(gomock.Any(), account).Return(nil) @@ -204,9 +204,9 @@ func TestSetupOwner_CreatePATFails_RollsBackSetupAccountAndUser(t *testing.T) { }, }, &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, userAuth auth.UserAuth) (string, error) { + GetAccountIDFromUserAuthFunc: func(_ context.Context, userAuth auth.UserAuth) (string, string, error) { assert.Equal(t, "owner-id", userAuth.UserId) - return "acc-1", nil + return "acc-1", "owner-id", nil }, CreatePATFunc: func(_ context.Context, accountID, initiatorUserID, targetUserID, tokenName string, expiresIn int) (*types.PersonalAccessTokenGenerated, error) { assert.Equal(t, "acc-1", accountID) @@ -233,6 +233,59 @@ func TestSetupOwner_CreatePATFails_RollsBackSetupAccountAndUser(t *testing.T) { assert.Equal(t, 1, rollbackCalls) } +// A concurrent first login can make the setup account shared: either a JIT user +// created the primary account first and the owner joined it, or a JIT user joined +// the setup account. A PAT failure must not delete an account other users rely on. +// DeleteAccount is not expected, so the mock store fails the test if it is called. +func TestSetupOwner_CreatePATFails_KeepsSharedAccount(t *testing.T) { + tests := map[string]*types.Account{ + "created by another user": {Id: "acc-1", CreatedBy: "jit-user"}, + "another user joined": {Id: "acc-1", CreatedBy: "owner-id", Users: map[string]*types.User{ + "owner-id": {Id: "owner-id"}, + "jit-user": {Id: "jit-user"}, + }}, + } + + for name, account := range tests { + t.Run(name, func(t *testing.T) { + t.Setenv(SetupPATEnabledEnvKey, "true") + + ctrl := gomock.NewController(t) + accountStore := nbstore.NewMockStore(ctrl) + accountStore.EXPECT().GetAccount(gomock.Any(), "acc-1").Return(account, nil) + + rollbackCalls := 0 + setupManager := NewSetupService( + &setupInstanceManagerMock{ + rollbackSetupFn: func(_ context.Context, _ string) error { + rollbackCalls++ + return nil + }, + }, + &mock_server.MockAccountManager{ + GetAccountIDFromUserAuthFunc: func(_ context.Context, _ auth.UserAuth) (string, string, error) { + return "acc-1", "owner-id", nil + }, + CreatePATFunc: func(_ context.Context, _, _, _, _ string, _ int) (*types.PersonalAccessTokenGenerated, error) { + return nil, status.Errorf(status.PermissionDenied, "user is blocked") + }, + GetStoreFunc: func() nbstore.Store { + return accountStore + }, + }, + ) + + _, err := setupManager.SetupOwner(context.Background(), "admin@example.com", "securepassword123", "Admin", SetupOptions{ + CreatePAT: true, + PATExpireInDays: intPtr(30), + }) + + require.Error(t, err) + assert.Equal(t, 1, rollbackCalls, "setup user should still be rolled back") + }) + } +} + func TestSetupOwner_CreatePATFails_AccountAlreadyGoneStillRollsBackUser(t *testing.T) { t.Setenv(SetupPATEnabledEnvKey, "true") @@ -251,8 +304,8 @@ func TestSetupOwner_CreatePATFails_AccountAlreadyGoneStillRollsBackUser(t *testi }, }, &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, _ auth.UserAuth) (string, error) { - return "acc-1", nil + GetAccountIDFromUserAuthFunc: func(_ context.Context, _ auth.UserAuth) (string, string, error) { + return "acc-1", "owner-id", nil }, CreatePATFunc: func(_ context.Context, _, _, _, _ string, _ int) (*types.PersonalAccessTokenGenerated, error) { return nil, errors.New("token failure") @@ -280,7 +333,7 @@ func TestSetupOwner_CreatePATFails_AccountRollbackFailureStopsBeforeUserRollback ctrl := gomock.NewController(t) accountStore := nbstore.NewMockStore(ctrl) - account := &types.Account{Id: "acc-1"} + account := &types.Account{Id: "acc-1", CreatedBy: "owner-id"} accountStore.EXPECT().GetAccount(gomock.Any(), "acc-1").Return(account, nil) accountStore.EXPECT().DeleteAccount(gomock.Any(), account).Return(errors.New("delete failed")) @@ -293,8 +346,8 @@ func TestSetupOwner_CreatePATFails_AccountRollbackFailureStopsBeforeUserRollback }, }, &mock_server.MockAccountManager{ - GetAccountIDByUserIdFunc: func(_ context.Context, _ auth.UserAuth) (string, error) { - return "acc-1", nil + GetAccountIDFromUserAuthFunc: func(_ context.Context, _ auth.UserAuth) (string, string, error) { + return "acc-1", "owner-id", nil }, CreatePATFunc: func(_ context.Context, _, _, _, _ string, _ int) (*types.PersonalAccessTokenGenerated, error) { return nil, errors.New("token failure") diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go index 5928a8ed2..63d3ef280 100644 --- a/management/server/integrated_validator.go +++ b/management/server/integrated_validator.go @@ -167,6 +167,10 @@ func (MockIntegratedValidator) SetPeerInvalidationListener(func(accountID string // just a dummy } +func (MockIntegratedValidator) SetPeerValidationChangeListener(func(accountID string, peerIDs []string)) { + // just a dummy +} + func (MockIntegratedValidator) Stop(_ context.Context) { // just a dummy } diff --git a/management/server/integrations/integrated_validator/integrated_validator_mock.go b/management/server/integrations/integrated_validator/integrated_validator_mock.go index 73178a869..27f61a79c 100644 --- a/management/server/integrations/integrated_validator/integrated_validator_mock.go +++ b/management/server/integrations/integrated_validator/integrated_validator_mock.go @@ -130,6 +130,18 @@ func (mr *MockIntegratedValidatorMockRecorder) SetPeerInvalidationListener(fn an return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPeerInvalidationListener", reflect.TypeOf((*MockIntegratedValidator)(nil).SetPeerInvalidationListener), fn) } +// SetPeerValidationChangeListener mocks base method. +func (m *MockIntegratedValidator) SetPeerValidationChangeListener(fn func(string, []string)) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetPeerValidationChangeListener", fn) +} + +// SetPeerValidationChangeListener indicates an expected call of SetPeerValidationChangeListener. +func (mr *MockIntegratedValidatorMockRecorder) SetPeerValidationChangeListener(fn any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPeerValidationChangeListener", reflect.TypeOf((*MockIntegratedValidator)(nil).SetPeerValidationChangeListener), fn) +} + // Stop mocks base method. func (m *MockIntegratedValidator) Stop(ctx context.Context) { m.ctrl.T.Helper() diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go index dc3332177..eb5ce8815 100644 --- a/management/server/integrations/integrated_validator/interface.go +++ b/management/server/integrations/integrated_validator/interface.go @@ -21,6 +21,9 @@ type IntegratedValidator interface { GetInvalidPeers(ctx context.Context, accountID string, extraSettings *types.ExtraSettings) (map[string]string, error) PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error SetPeerInvalidationListener(fn func(accountID string, peerIDs []string)) + // SetPeerValidationChangeListener registers fn to be called with the peers whose + // validation state changed outside a login or sync, so their network maps can be pushed. + SetPeerValidationChangeListener(fn func(accountID string, peerIDs []string)) Stop(ctx context.Context) ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow } diff --git a/management/server/integrations/integrated_validator/validator/validator.go b/management/server/integrations/integrated_validator/validator/validator.go index 33199c065..ec4c4827d 100644 --- a/management/server/integrations/integrated_validator/validator/validator.go +++ b/management/server/integrations/integrated_validator/validator/validator.go @@ -55,6 +55,9 @@ func (v *IntegratedValidatorImpl) PeerDeleted(_ context.Context, _, _ string, _ func (v *IntegratedValidatorImpl) SetPeerInvalidationListener(_ func(accountID string, peerIDs []string)) { } +func (v *IntegratedValidatorImpl) SetPeerValidationChangeListener(_ func(accountID string, peerIDs []string)) { +} + func (v *IntegratedValidatorImpl) Stop(_ context.Context) { } diff --git a/management/server/peer.go b/management/server/peer.go index aa3abcb86..5b33ca9f8 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -140,7 +140,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(ctx context.Context, peerPubK return nil } -// schedulePeerExpirations reschedules the account's login/inactivity expiration +// schedulePeerExpirations arms the account's login/inactivity expiration // timers for an SSO peer that just connected. func (am *DefaultAccountManager) schedulePeerExpirations(ctx context.Context, accountID string, peer *nbpeer.Peer) error { if !peer.AddedWithSSOLogin() { @@ -154,7 +154,7 @@ func (am *DefaultAccountManager) schedulePeerExpirations(ctx context.Context, ac am.schedulePeerLoginExpiration(ctx, accountID) } if peer.InactivityExpirationEnabled && settings.PeerInactivityExpirationEnabled { - am.checkAndSchedulePeerInactivityExpiration(ctx, accountID) + am.schedulePeerInactivityExpiration(ctx, accountID) } return nil } diff --git a/management/server/peer_validation_change_test.go b/management/server/peer_validation_change_test.go new file mode 100644 index 000000000..14c179fee --- /dev/null +++ b/management/server/peer_validation_change_test.go @@ -0,0 +1,59 @@ +package server + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/netbirdio/netbird/management/internals/controllers/network_map" + "github.com/netbirdio/netbird/management/server/types" +) + +// A peer that the background validator approves must get its own map and appear in the +// maps of the peers a policy connects it to, without waiting for a login or sync. +func TestOnPeerValidationChanged_UpdatesChangedPeerAndItsPeers(t *testing.T) { + _, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t) + ctx := context.Background() + + err := s.CreatePolicy(ctx, &types.Policy{ + ID: "policy-validation-change", + AccountID: accountID, + Enabled: true, + Rules: []*types.PolicyRule{{ + ID: "rule-validation-change", + PolicyID: "policy-validation-change", + Enabled: true, + Sources: []string{groupIDs[0]}, + Destinations: []string{groupIDs[1]}, + Bidirectional: true, + Action: types.PolicyTrafficActionAccept, + }}, + }) + require.NoError(t, err) + + var changed, affected []string + controller := network_map.NewMockController(gomock.NewController(t)) + controller.EXPECT().OnPeersUpdated(gomock.Any(), accountID, gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, peerIDs, affectedPeerIDs []string) error { + changed, affected = peerIDs, affectedPeerIDs + return nil + }) + // A separate manager over the same store: the setup's manager still has peer update + // goroutines in flight that read its controller. + manager := &DefaultAccountManager{Store: s, networkMapController: controller} + + manager.onPeerValidationChanged(ctx, accountID, []string{peerIDs[0]}) + + assert.Equal(t, []string{peerIDs[0]}, changed, "changed peers should be passed through") + assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, affected, "the changed peer and its policy peer should be updated") +} + +func TestOnPeerValidationChanged_NoPeersNoUpdate(t *testing.T) { + // Any call on the mock fails the test. + manager := &DefaultAccountManager{networkMapController: network_map.NewMockController(gomock.NewController(t))} + + manager.onPeerValidationChanged(context.Background(), "account", nil) +} diff --git a/management/server/store/sql_store_personal_access_token.go b/management/server/store/sql_store_personal_access_token.go index d5be3e327..67afc4d09 100644 --- a/management/server/store/sql_store_personal_access_token.go +++ b/management/server/store/sql_store_personal_access_token.go @@ -12,7 +12,6 @@ import ( "gorm.io/gorm/clause" "github.com/netbirdio/netbird/management/server/types" - "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/shared/management/status" ) @@ -130,22 +129,29 @@ func (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength return pats, nil } -// MarkPATUsed marks a personal access token as used. +// MarkPATUsed marks a personal access token as used. The write is skipped when LastUsed is newer than +// types.PATLastUsedUpdateInterval, which also turns concurrent calls for the same token into no-ops. It returns a +// NotFound error when the token doesn't exist. func (s *SqlStore) MarkPATUsed(ctx context.Context, patID string) error { - patCopy := types.PersonalAccessToken{ - LastUsed: util.ToPtr(time.Now().UTC()), - } - - fieldsToUpdate := []string{"last_used"} - result := s.db.Select(fieldsToUpdate). - Where(idQueryCondition, patID).Updates(&patCopy) + now := time.Now().UTC() + result := s.db.Model(&types.PersonalAccessToken{}). + Where(idQueryCondition, patID). + Where("last_used IS NULL OR last_used < ?", now.Add(-types.PATLastUsedUpdateInterval)). + Update("last_used", now) if result.Error != nil { log.WithContext(ctx).Errorf("failed to mark pat as used: %s", result.Error) return status.Errorf(status.Internal, "failed to mark pat as used") } if result.RowsAffected == 0 { - return status.NewPATNotFoundError(patID) + var count int64 + if err := s.db.Model(&types.PersonalAccessToken{}).Where(idQueryCondition, patID).Count(&count).Error; err != nil { + log.WithContext(ctx).Errorf("failed to check pat existence: %s", err) + return status.Errorf(status.Internal, "failed to mark pat as used") + } + if count == 0 { + return status.NewPATNotFoundError(patID) + } } return nil diff --git a/management/server/store/sql_store_personal_access_token_test.go b/management/server/store/sql_store_personal_access_token_test.go index f40e0d9c6..4c2d97014 100644 --- a/management/server/store/sql_store_personal_access_token_test.go +++ b/management/server/store/sql_store_personal_access_token_test.go @@ -136,6 +136,25 @@ func TestSqlStore_MarkPATUsed(t *testing.T) { require.NoError(t, err) now := time.Now().UTC() require.WithinRange(t, pat.LastUsed.UTC(), now.Add(-15*time.Second), now, "LastUsed should be within 1 second of now") + + recent := now.Add(-types.PATLastUsedUpdateInterval / 2).Truncate(time.Second) + require.NoError(t, store.(*SqlStore).db.Model(&types.PersonalAccessToken{}).Where("id = ?", patID).Update("last_used", recent).Error) + require.NoError(t, store.MarkPATUsed(context.Background(), patID)) + pat, err = store.GetPATByID(context.Background(), LockingStrengthNone, userID, patID) + require.NoError(t, err) + assert.True(t, recent.Equal(pat.LastUsed.UTC()), "LastUsed within the update interval must not be rewritten") + + stale := now.Add(-2 * types.PATLastUsedUpdateInterval) + require.NoError(t, store.(*SqlStore).db.Model(&types.PersonalAccessToken{}).Where("id = ?", patID).Update("last_used", stale).Error) + require.NoError(t, store.MarkPATUsed(context.Background(), patID)) + pat, err = store.GetPATByID(context.Background(), LockingStrengthNone, userID, patID) + require.NoError(t, err) + assert.True(t, pat.LastUsed.UTC().After(stale), "LastUsed older than the update interval must be rewritten") + + err = store.MarkPATUsed(context.Background(), "unknown-pat") + sErr, ok := status.FromError(err) + require.True(t, ok, "unknown pat must return a status error") + assert.Equal(t, status.NotFound, sErr.Type()) } func TestSqlStore_SavePAT(t *testing.T) { diff --git a/management/server/store/sql_store_user.go b/management/server/store/sql_store_user.go index 4db03fb71..111ce08ff 100644 --- a/management/server/store/sql_store_user.go +++ b/management/server/store/sql_store_user.go @@ -252,20 +252,32 @@ func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength Lockin return accountID, nil } -// SaveUserLastLogin stores the last login time for a user in DB. +// SaveUserLastLogin stores the last login time for a user in DB. It only writes when the stored value is unset or +// older than lastLogin, so repeating the same login doesn't rewrite the row and an older login never overwrites a newer one. +// It returns a NotFound error when the user doesn't exist. func (s *SqlStore) SaveUserLastLogin(ctx context.Context, accountID, userID string, lastLogin time.Time) error { - var user types.User - result := s.db.Take(&user, accountAndIDQueryCondition, accountID, userID) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return status.NewUserNotFoundError(userID) - } - return status.NewGetUserFromStoreError() + if lastLogin.IsZero() { + return nil } - if !lastLogin.IsZero() { - user.LastLogin = &lastLogin - return s.db.Save(&user).Error + result := s.db.Model(&types.User{}). + Where(accountAndIDQueryCondition, accountID, userID). + Where("last_login IS NULL OR last_login < ?", lastLogin). + Update("last_login", lastLogin) + if result.Error != nil { + log.WithContext(ctx).Errorf("failed to save user last login: %s", result.Error) + return status.Errorf(status.Internal, "failed to save user last login") + } + + if result.RowsAffected == 0 { + var count int64 + if err := s.db.Model(&types.User{}).Where(accountAndIDQueryCondition, accountID, userID).Count(&count).Error; err != nil { + log.WithContext(ctx).Errorf("failed to check user existence: %s", err) + return status.NewGetUserFromStoreError() + } + if count == 0 { + return status.NewUserNotFoundError(userID) + } } return nil diff --git a/management/server/store/sql_store_user_test.go b/management/server/store/sql_store_user_test.go index 34bc458fe..d5ee14a6a 100644 --- a/management/server/store/sql_store_user_test.go +++ b/management/server/store/sql_store_user_test.go @@ -341,3 +341,50 @@ func TestSqlStore_SaveUsers_LargeBatch(t *testing.T) { require.NoError(t, err) require.Equal(t, 8002, len(accountUsers)) } + +func TestSqlStore_SaveUserLastLogin(t *testing.T) { + store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/extended-store.sql", t.TempDir()) + t.Cleanup(cleanup) + require.NoError(t, err) + + ctx := context.Background() + accountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + userID := "f4f6d672-63fb-11ec-90d6-0242ac120003" + + before, err := store.GetUserByUserID(ctx, LockingStrengthNone, userID) + require.NoError(t, err) + + lastLoginOf := func() time.Time { + user, err := store.GetUserByUserID(ctx, LockingStrengthNone, userID) + require.NoError(t, err) + return user.GetLastLogin().UTC() + } + + first := time.Now().UTC().Truncate(time.Second) + require.NoError(t, store.SaveUserLastLogin(ctx, accountID, userID, first)) + assert.True(t, first.Equal(lastLoginOf()), "first login must be stored") + + require.NoError(t, store.SaveUserLastLogin(ctx, accountID, userID, first)) + assert.True(t, first.Equal(lastLoginOf()), "repeating the same login must keep the stored value") + + later := first.Add(30 * time.Second) + require.NoError(t, store.SaveUserLastLogin(ctx, accountID, userID, later)) + assert.True(t, later.Equal(lastLoginOf()), "a newer login must be written") + + require.NoError(t, store.SaveUserLastLogin(ctx, accountID, userID, first)) + assert.True(t, later.Equal(lastLoginOf()), "an older login must not overwrite a newer one") + + require.NoError(t, store.SaveUserLastLogin(ctx, accountID, userID, time.Time{})) + assert.True(t, later.Equal(lastLoginOf()), "zero login must be ignored") + + err = store.SaveUserLastLogin(ctx, accountID, "unknown-user", later.Add(time.Hour)) + sErr, ok := status.FromError(err) + require.True(t, ok, "unknown user must return a status error") + assert.Equal(t, status.NotFound, sErr.Type()) + + after, err := store.GetUserByUserID(ctx, LockingStrengthNone, userID) + require.NoError(t, err) + assert.Equal(t, before.Role, after.Role) + assert.Equal(t, before.AutoGroups, after.AutoGroups) + assert.Equal(t, before.Blocked, after.Blocked) +} diff --git a/management/server/types/group.go b/management/server/types/group.go index ac0a2a7f2..84fcda828 100644 --- a/management/server/types/group.go +++ b/management/server/types/group.go @@ -40,7 +40,7 @@ type Group struct { type GroupPeer struct { AccountID string `gorm:"index"` GroupID string `gorm:"primaryKey"` - PeerID string `gorm:"primaryKey"` + PeerID string `gorm:"primaryKey;index:idx_group_peers_peer_id,option:CONCURRENTLY"` } func (g *Group) LoadGroupPeers() { diff --git a/management/server/types/personal_access_token.go b/management/server/types/personal_access_token.go index 0aa6b152b..9478be5b2 100644 --- a/management/server/types/personal_access_token.go +++ b/management/server/types/personal_access_token.go @@ -23,6 +23,8 @@ const ( PATChecksumLength = 6 // PATLength total number of characters used for the token PATLength = 40 + // PATLastUsedUpdateInterval is the minimum age of a token's LastUsed before it is written again + PATLastUsedUpdateInterval = time.Minute ) // PersonalAccessToken holds all information about a PAT including a hashed version of it for verification @@ -31,7 +33,7 @@ type PersonalAccessToken struct { // User is a reference to Account that this object belongs UserID string `gorm:"index"` Name string - HashedToken string + HashedToken string `gorm:"index:idx_personal_access_tokens_hashed_token"` ExpirationDate *time.Time // scope could be added in future CreatedBy string diff --git a/management/server/user.go b/management/server/user.go index e64320666..3e84fcd03 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -102,11 +102,11 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u inviterID := userID if initiatorUser.IsServiceUser { - createdBy, err := am.Store.GetAccountCreatedBy(ctx, store.LockingStrengthNone, accountID) + owner, err := am.Store.GetAccountOwner(ctx, store.LockingStrengthNone, accountID) if err != nil { return nil, err } - inviterID = createdBy + inviterID = owner.Id } var idpUser *idp.UserData @@ -238,9 +238,10 @@ func (am *DefaultAccountManager) GetUserFromUserAuth(ctx context.Context, userAu // server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event. newLogin := user.LastDashboardLoginChanged(userAuth.LastLogin) - err = am.Store.SaveUserLastLogin(ctx, userAuth.AccountId, userAuth.UserId, userAuth.LastLogin) - if err != nil { - log.WithContext(ctx).Debugf("failed to update user last login: %v", err) + if userAuth.LastLogin.After(user.GetLastLogin()) { + if err := am.Store.SaveUserLastLogin(ctx, userAuth.AccountId, userAuth.UserId, userAuth.LastLogin); err != nil { + log.WithContext(ctx).Debugf("failed to update user last login: %v", err) + } } if newLogin { diff --git a/management/server/user_test.go b/management/server/user_test.go index fcfb7a1e7..305ed5a24 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -706,6 +706,69 @@ func TestUser_InviteNewUser(t *testing.T) { assert.Errorf(t, err, "Invite user with owner role should throw error") } +func TestUser_InviteNewUser_ServiceUserAfterCreatorRemoved(t *testing.T) { + s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + require.NoError(t, err) + t.Cleanup(cleanup) + + const ( + creatorID = "creatorID" + newOwnerID = "newOwnerID" + newOwnerMail = "new-owner@test.com" + ) + + account := newAccountWithId(context.Background(), mockAccountID, creatorID, "", "", "", false) + delete(account.Users, creatorID) + account.Users[newOwnerID] = &types.User{ + Id: newOwnerID, + AccountID: mockAccountID, + Role: types.UserRoleOwner, + } + account.Users[mockServiceUserID] = &types.User{ + Id: mockServiceUserID, + AccountID: mockAccountID, + Role: types.UserRoleAdmin, + IsServiceUser: true, + ServiceUserName: mockServiceUserName, + } + require.NoError(t, s.SaveAccount(context.Background(), account)) + + am := DefaultAccountManager{ + Store: s, + eventStore: &activity.InMemoryEventStore{}, + cacheLoading: map[string]chan struct{}{}, + permissionsManager: permissions.NewManager(s), + } + + cs, err := nbcache.NewStore(context.Background(), nbcache.DefaultIDPCacheExpirationMax, nbcache.DefaultIDPCacheCleanupInterval, nbcache.DefaultIDPCacheOpenConn) + require.NoError(t, err) + cacheManager := nbcache.NewAccountUserDataCache(am.loadAccount, cs) + t.Cleanup(func() { assert.NoError(t, cacheManager.Close()) }) + am.cacheManager = cacheManager + + idpUsers := []*idp.UserData{{ID: newOwnerID, Email: newOwnerMail, Name: "new owner"}} + var invitedBy string + am.idpManager = &idp.MockIDP{ + CreateUserFunc: func(_ context.Context, email, name, _, invitedByEmail string) (*idp.UserData, error) { + invitedBy = invitedByEmail + newUser := &idp.UserData{ID: "invitedID", Email: email, Name: name} + idpUsers = append(idpUsers, newUser) + return newUser, nil + }, + GetAccountFunc: func(_ context.Context, _ string) ([]*idp.UserData, error) { + return idpUsers, nil + }, + } + + _, err = am.inviteNewUser(context.Background(), mockAccountID, mockServiceUserID, &types.UserInfo{ + Name: "invited", + Email: "invited@test.com", + Role: mockRole, + }) + require.NoError(t, err) + assert.Equal(t, newOwnerMail, invitedBy, "service user invites should be sent on behalf of the current owner") +} + func TestUser_DeleteUser_ServiceUser(t *testing.T) { tests := []struct { name string diff --git a/proxy/web/package-lock.json b/proxy/web/package-lock.json index 1611323a7..1e29722cc 100644 --- a/proxy/web/package-lock.json +++ b/proxy/web/package-lock.json @@ -29,17 +29,17 @@ "tsx": "^4.21.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "7.3.2" + "vite": "7.3.7" } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -48,9 +48,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -58,22 +58,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -90,14 +89,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -107,14 +106,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -124,9 +123,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -134,29 +133,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -176,9 +175,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -186,9 +185,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -196,9 +195,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -206,27 +205,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -268,33 +267,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -302,14 +301,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -915,29 +914,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1710,7 +1723,6 @@ "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1721,7 +1733,6 @@ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1781,7 +1792,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -1926,9 +1936,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -2033,7 +2043,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2099,19 +2108,22 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.11.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.30.tgz", + "integrity": "sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -2120,9 +2132,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", "dev": true, "funding": [ { @@ -2139,13 +2151,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -2165,9 +2176,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "version": "1.0.30001817", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001817.tgz", + "integrity": "sha512-uZ1JG80BwhtMalIAGPxPfmNhHyd/B/nTdr3hITWlRamSyAdUStP1Gop01e7SD5q6J90ixJvWDbvmiFVfDgn4bw==", "dev": true, "funding": [ { @@ -2303,9 +2314,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.463", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.463.tgz", + "integrity": "sha512-nVWNU6ltFjopvOdD44J4zoBkgO0M+WZiFKKFV7EjebOH9z0KNphk6bCEjplXtJofL21ARIuuwWOCsFkBVklm6w==", "dev": true, "license": "ISC" }, @@ -2394,7 +2405,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2846,10 +2856,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3263,9 +3283,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -3289,11 +3309,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.59", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", + "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -3391,7 +3414,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3400,9 +3422,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "dev": true, "funding": [ { @@ -3420,9 +3442,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3453,7 +3475,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3585,9 +3606,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3687,7 +3708,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -3721,7 +3741,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3762,9 +3781,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dev": true, "funding": [ { @@ -3803,14 +3822,13 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3930,7 +3948,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/proxy/web/package.json b/proxy/web/package.json index 9a7c84ed4..5c8e3e94e 100644 --- a/proxy/web/package.json +++ b/proxy/web/package.json @@ -31,6 +31,6 @@ "tsx": "^4.21.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "7.3.2" + "vite": "7.3.7" } } diff --git a/relay/metrics/realy.go b/relay/metrics/realy.go index 49a357557..1af7a1ca2 100644 --- a/relay/metrics/realy.go +++ b/relay/metrics/realy.go @@ -8,6 +8,8 @@ import ( log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" + + "github.com/netbirdio/netbird/shared/relay/messages" ) const ( @@ -17,16 +19,17 @@ const ( type Metrics struct { metric.Meter - TransferBytesSent metric.Int64Counter - TransferBytesRecv metric.Int64Counter - AuthenticationTime metric.Float64Histogram - PeerStoreTime metric.Float64Histogram - peerReconnections metric.Int64Counter - peers metric.Int64UpDownCounter - peerActivityChan chan string - peerLastActive map[string]time.Time - mutexActivity sync.Mutex - ctx context.Context + TransferBytesSent metric.Int64Counter + TransferBytesRecv metric.Int64Counter + AuthenticationTime metric.Float64Histogram + PeerStoreTime metric.Float64Histogram + peerReconnections metric.Int64Counter + peerProtocolVersions metric.Int64Counter + peers metric.Int64UpDownCounter + peerActivityChan chan string + peerLastActive map[string]time.Time + mutexActivity sync.Mutex + ctx context.Context } func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) { @@ -88,14 +91,22 @@ func NewMetrics(ctx context.Context, meter metric.Meter) (*Metrics, error) { return nil, err } + peerProtocolVersions, err := meter.Int64Counter("relay_peer_protocol_versions_total", + metric.WithDescription("Total number of peer connections by advertised and selected protocol version"), + ) + if err != nil { + return nil, err + } + m := &Metrics{ - Meter: meter, - TransferBytesSent: bytesSent, - TransferBytesRecv: bytesRecv, - AuthenticationTime: authTime, - PeerStoreTime: peerStoreTime, - peers: peers, - peerReconnections: peerReconnections, + Meter: meter, + TransferBytesSent: bytesSent, + TransferBytesRecv: bytesRecv, + AuthenticationTime: authTime, + PeerStoreTime: peerStoreTime, + peers: peers, + peerReconnections: peerReconnections, + peerProtocolVersions: peerProtocolVersions, ctx: ctx, peerActivityChan: make(chan string, 10), @@ -151,6 +162,15 @@ func (m *Metrics) RecordPeerReconnection() { m.peerReconnections.Add(m.ctx, 1) } +// RecordProtocolVersion counts a new peer connection by the protocol version the client advertised +// and the version the server selected for it +func (m *Metrics) RecordProtocolVersion(advertised, selected messages.ProtocolVersion) { + m.peerProtocolVersions.Add(m.ctx, 1, metric.WithAttributes( + attribute.Int("advertised", int(advertised)), + attribute.Int("selected", int(selected)), + )) +} + // PeerActivity increases the active connections func (m *Metrics) PeerActivity(peerID string) { select { diff --git a/relay/server/handshake.go b/relay/server/handshake.go index f064b3501..c5ec39283 100644 --- a/relay/server/handshake.go +++ b/relay/server/handshake.go @@ -19,20 +19,9 @@ type Validator interface { Validate(any) error } -// preparedMsg contains the marshalled success response message +// preparedMsg contains the marshalled success response message for every supported protocol version type preparedMsg struct { - responseAuthMsg []byte -} - -func newPreparedMsg(instanceURL string) (*preparedMsg, error) { - ram, err := messages.MarshalAuthResponse(instanceURL) - if err != nil { - return nil, fmt.Errorf("failed to marshal auth response msg: %w", err) - } - - return &preparedMsg{ - responseAuthMsg: ram, - }, nil + responseAuthMsg map[messages.ProtocolVersion][]byte } type handshake struct { @@ -40,7 +29,35 @@ type handshake struct { validator Validator preparedMsg *preparedMsg - peerID *messages.PeerID + peerID *messages.PeerID + advertisedVersion messages.ProtocolVersion + selectedVersion messages.ProtocolVersion +} + +func newPreparedMsg(instanceURL string) (*preparedMsg, error) { + responses := make(map[messages.ProtocolVersion][]byte, messages.CurrentProtocolVersion) + for version := messages.ProtocolV1; ; version++ { + ram, err := messages.NewEncoder(version).AuthResponse(instanceURL) + if err != nil { + return nil, fmt.Errorf("marshal auth response for version %d: %w", version, err) + } + responses[version] = ram + if version == messages.CurrentProtocolVersion { + break + } + } + + return &preparedMsg{ + responseAuthMsg: responses, + }, nil +} + +func (p *preparedMsg) authResponse(version messages.ProtocolVersion) ([]byte, error) { + msg, ok := p.responseAuthMsg[version] + if !ok { + return nil, fmt.Errorf("no prepared auth response for protocol version %d", version) + } + return msg, nil } func (h *handshake) handshakeReceive(ctx context.Context) (*messages.PeerID, error) { @@ -52,9 +69,9 @@ func (h *handshake) handshakeReceive(ctx context.Context) (*messages.PeerID, err buf = buf[:n] - _, err = messages.ValidateVersion(buf) + advertised, err := messages.ReadVersion(buf) if err != nil { - return nil, fmt.Errorf("validate version from %s: %w", h.conn.RemoteAddr(), err) + return nil, fmt.Errorf("read version from %s: %w", h.conn.RemoteAddr(), err) } msgType, err := messages.DetermineClientMessageType(buf) @@ -70,12 +87,20 @@ func (h *handshake) handshakeReceive(ctx context.Context) (*messages.PeerID, err if err != nil { return peerID, err } + h.peerID = peerID + h.advertisedVersion = advertised + h.selectedVersion = messages.SelectVersion(advertised, messages.CurrentProtocolVersion) return peerID, nil } func (h *handshake) handshakeResponse(ctx context.Context) error { - if _, err := h.conn.Write(ctx, h.preparedMsg.responseAuthMsg); err != nil { + msg, err := h.preparedMsg.authResponse(h.selectedVersion) + if err != nil { + return err + } + + if _, err := h.conn.Write(ctx, msg); err != nil { return fmt.Errorf("handshake response write to %s (%s): %w", h.peerID, h.conn.RemoteAddr(), err) } diff --git a/relay/server/handshake_test.go b/relay/server/handshake_test.go new file mode 100644 index 000000000..5cdcaa8ea --- /dev/null +++ b/relay/server/handshake_test.go @@ -0,0 +1,378 @@ +package server + +import ( + "context" + "encoding/binary" + "errors" + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/relay/auth/allow" + "github.com/netbirdio/netbird/shared/relay/messages" +) + +const ( + testWaitTimeout = 2 * time.Second + + // authTokenLenOffset is where the envelope's token length starts: header(2) + magic(4) + peer ID(36). + authTokenLenOffset = 2 + 4 + 36 +) + +// scriptedConn is a listener.Conn whose inbound frames are queued by the test and whose +// outbound frames are recorded. +type scriptedConn struct { + reads chan []byte + closed chan struct{} + closeOnce sync.Once + onWrite func([]byte) + + mu sync.Mutex + writes [][]byte +} + +type recordingValidator struct { + err error + + mu sync.Mutex + payloads [][]byte +} + +func newScriptedConn() *scriptedConn { + return &scriptedConn{ + reads: make(chan []byte, 16), + closed: make(chan struct{}), + } +} + +func (c *scriptedConn) Read(ctx context.Context, b []byte) (int, error) { + select { + case msg := <-c.reads: + return copy(b, msg), nil + case <-c.closed: + return 0, net.ErrClosed + case <-ctx.Done(): + return 0, ctx.Err() + } +} + +func (c *scriptedConn) Write(_ context.Context, b []byte) (int, error) { + select { + case <-c.closed: + return 0, net.ErrClosed + default: + } + + frame := append([]byte(nil), b...) + c.mu.Lock() + c.writes = append(c.writes, frame) + c.mu.Unlock() + + if c.onWrite != nil { + c.onWrite(frame) + } + return len(b), nil +} + +func (c *scriptedConn) RemoteAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1} +} + +func (c *scriptedConn) Close() error { + c.closeOnce.Do(func() { close(c.closed) }) + return nil +} + +func (c *scriptedConn) Protocol() string { + return "test" +} + +func (c *scriptedConn) deliver(t *testing.T, frame []byte) { + t.Helper() + select { + case c.reads <- frame: + case <-time.After(testWaitTimeout): + t.Fatal("scripted connection read queue is full") + } +} + +func (c *scriptedConn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} + +func (c *scriptedConn) frames() [][]byte { + c.mu.Lock() + defer c.mu.Unlock() + return append([][]byte(nil), c.writes...) +} + +func (c *scriptedConn) waitFrames(t *testing.T, n int) [][]byte { + t.Helper() + deadline := time.Now().Add(testWaitTimeout) + for { + frames := c.frames() + if len(frames) >= n { + return frames + } + if time.Now().After(deadline) { + t.Fatalf("expected %d written frames, got %d", n, len(frames)) + } + time.Sleep(5 * time.Millisecond) + } +} + +func (c *scriptedConn) waitClosed(t *testing.T) { + t.Helper() + select { + case <-c.closed: + case <-time.After(testWaitTimeout): + t.Fatal("connection was not closed") + } +} + +func (v *recordingValidator) Validate(credentials any) error { + v.mu.Lock() + defer v.mu.Unlock() + + payload, _ := credentials.([]byte) + v.payloads = append(v.payloads, append([]byte(nil), payload...)) + return v.err +} + +func (v *recordingValidator) calls() [][]byte { + v.mu.Lock() + defer v.mu.Unlock() + return append([][]byte(nil), v.payloads...) +} + +func TestRelay_AuthV1ResponseIsUnchanged(t *testing.T) { + validator := &recordingValidator{} + relay := newTestRelay(t, validator) + peerID := messages.HashID("v1-client") + token := []byte("v1-token") + auth, err := messages.MarshalAuthMsg(peerID, token) + require.NoError(t, err) + + conn := newScriptedConn() + conn.deliver(t, auth) + relay.Accept(conn) + + frames := conn.waitFrames(t, 1) + want, err := messages.MarshalAuthResponse(relay.instanceURL.String()) + require.NoError(t, err) + assert.Equal(t, want, frames[0], "a v1 client must receive the same response bytes as today") + assert.Equal(t, [][]byte{token}, validator.calls(), "the validator must receive the v1 token bytes") + assert.False(t, conn.isClosed(), "an authenticated v1 connection must stay open") + + item, ok := relay.store.Peer(peerID) + require.True(t, ok, "the peer must be stored after the response") + assert.Equal(t, messages.ProtocolV1, item.(*Peer).Version(), "a v1 client selects v1") +} + +func TestRelay_AuthEnvelopeSelectsV1(t *testing.T) { + cases := []struct { + name string + version messages.ProtocolVersion + ext []byte + }{ + {"v2 without extension", 2, nil}, + {"v2 with json extension", 2, []byte(`{"cred2":"value"}`)}, + {"v255 with opaque extension", 255, []byte("opaque-extension-bytes")}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + validator := &recordingValidator{} + relay := newTestRelay(t, validator) + peerID := messages.HashID("future-client") + token := []byte("future-token") + auth, err := messages.MarshalAuthEnvelope(tc.version, peerID, token, tc.ext) + require.NoError(t, err) + + conn := newScriptedConn() + conn.deliver(t, auth) + relay.Accept(conn) + + frames := conn.waitFrames(t, 1) + want, err := messages.MarshalAuthResponse(relay.instanceURL.String()) + require.NoError(t, err) + assert.Equal(t, want, frames[0], "the response must carry the selected v1 header") + assert.Equal(t, [][]byte{token}, validator.calls(), "the validator must receive the token without the extension") + assert.False(t, conn.isClosed(), "a higher advertised version must not be rejected") + + item, ok := relay.store.Peer(peerID) + require.True(t, ok, "the peer must be stored after the response") + assert.Equal(t, messages.ProtocolV1, item.(*Peer).Version(), "the connection must use the selected v1") + }) + } +} + +func TestRelay_AuthRejectsInvalidFirstFrame(t *testing.T) { + peerID := messages.HashID("client") + validEnvelope, err := messages.MarshalAuthEnvelope(2, peerID, []byte("token"), []byte("ext")) + require.NoError(t, err) + validV1, err := messages.MarshalAuthMsg(peerID, []byte("token")) + require.NoError(t, err) + transportFirst, err := messages.MarshalTransportMsg(peerID, []byte("payload")) + require.NoError(t, err) + + versionZero := append([]byte(nil), validV1...) + versionZero[0] = 0 + + badMagic := append([]byte(nil), validEnvelope...) + badMagic[2] ^= 0xFF + + zeroTokenLen := append([]byte(nil), validEnvelope...) + binary.BigEndian.PutUint16(zeroTokenLen[authTokenLenOffset:], 0) + + tokenLenPastEnd := append([]byte(nil), validEnvelope...) + binary.BigEndian.PutUint16(tokenLenPastEnd[authTokenLenOffset:], uint16(len(validEnvelope))) + + cases := []struct { + name string + frame []byte + }{ + {"version zero", versionZero}, + {"reserved hello type", []byte{byte(messages.ProtocolV1), byte(messages.MsgTypeHello)}}, + {"unknown type before auth", []byte{byte(messages.ProtocolV1), 200}}, + {"transport before auth", transportFirst}, + {"bad magic in envelope", badMagic}, + {"zero token length", zeroTokenLen}, + {"token length past the frame", tokenLenPastEnd}, + {"envelope truncated at the length field", validEnvelope[:authTokenLenOffset+1]}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + validator := &recordingValidator{} + relay := newTestRelay(t, validator) + + conn := newScriptedConn() + conn.deliver(t, tc.frame) + relay.Accept(conn) + + assert.True(t, conn.isClosed(), "the connection must be closed") + assert.Empty(t, conn.frames(), "no response may be written") + assert.Empty(t, validator.calls(), "the validator must not run") + _, ok := relay.store.Peer(peerID) + assert.False(t, ok, "the peer must not be stored") + }) + } +} + +func TestRelay_AuthRejectsInvalidToken(t *testing.T) { + peerID := messages.HashID("client") + v1Auth, err := messages.MarshalAuthMsg(peerID, []byte("token")) + require.NoError(t, err) + envelopeAuth, err := messages.MarshalAuthEnvelope(2, peerID, []byte("token"), []byte(`{"ok":true}`)) + require.NoError(t, err) + + cases := []struct { + name string + frame []byte + }{ + {"v1 layout", v1Auth}, + {"envelope with well-formed extension", envelopeAuth}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + validator := &recordingValidator{err: errors.New("invalid token")} + relay := newTestRelay(t, validator) + + conn := newScriptedConn() + conn.deliver(t, tc.frame) + relay.Accept(conn) + + assert.True(t, conn.isClosed(), "the connection must be closed") + assert.Empty(t, conn.frames(), "no response may be written") + assert.Len(t, validator.calls(), 1, "the validator must run exactly once") + _, ok := relay.store.Peer(peerID) + assert.False(t, ok, "the peer must not be stored") + }) + } +} + +func TestRelay_AuthResponsePrecedesPublish(t *testing.T) { + relay := newTestRelay(t, &allow.Auth{}) + peerID := messages.HashID("client") + + onlineEvents := make(chan []messages.PeerID, 1) + watcher := relay.notifier.NewListener(func(ids []messages.PeerID) { onlineEvents <- ids }, func([]messages.PeerID) {}) + t.Cleanup(func() { relay.notifier.RemoveListener(watcher) }) + watcher.AddInterestedPeers([]messages.PeerID{peerID}) + + var storedAtResponse, notifiedAtResponse bool + conn := newScriptedConn() + conn.onWrite = func(frame []byte) { + if frame[1] != byte(messages.MsgTypeAuthResponse) { + return + } + _, storedAtResponse = relay.store.Peer(peerID) + notifiedAtResponse = len(onlineEvents) > 0 + } + + auth, err := messages.MarshalAuthMsg(peerID, []byte("token")) + require.NoError(t, err) + conn.deliver(t, auth) + relay.Accept(conn) + + conn.waitFrames(t, 1) + assert.False(t, storedAtResponse, "the peer must not be in the store when the response is written") + assert.False(t, notifiedAtResponse, "no online notification may precede the response") + + select { + case ids := <-onlineEvents: + assert.Equal(t, []messages.PeerID{peerID}, ids, "the peer must be published after the response") + case <-time.After(testWaitTimeout): + t.Fatal("the peer was not published after the response") + } +} + +func TestRelay_ReconnectClosesPreviousConnection(t *testing.T) { + relay := newTestRelay(t, &allow.Auth{}) + first, peerID := acceptPeer(t, relay, "client") + + auth, err := messages.MarshalAuthMsg(peerID, []byte("token")) + require.NoError(t, err) + second := newScriptedConn() + second.deliver(t, auth) + relay.Accept(second) + + second.waitFrames(t, 1) + first.waitClosed(t) + assert.False(t, second.isClosed(), "the reconnecting client must keep its new connection") + item, ok := relay.store.Peer(peerID) + require.True(t, ok, "the peer must stay stored after a reconnect") + assert.Same(t, second, item.(*Peer).conn, "the store must hold the new connection") +} + +func newTestRelay(t *testing.T, validator Validator) *Relay { + t.Helper() + relay, err := NewRelay(Config{ + ExposedAddress: "rel://127.0.0.1:1234", + AuthValidator: validator, + }) + require.NoError(t, err) + t.Cleanup(func() { relay.Shutdown(context.Background()) }) + return relay +} + +func acceptPeer(t *testing.T, relay *Relay, name string) (*scriptedConn, messages.PeerID) { + t.Helper() + peerID := messages.HashID(name) + auth, err := messages.MarshalAuthMsg(peerID, []byte("token")) + require.NoError(t, err) + + conn := newScriptedConn() + conn.deliver(t, auth) + relay.Accept(conn) + conn.waitFrames(t, 1) + return conn, peerID +} diff --git a/relay/server/peer.go b/relay/server/peer.go index 8376cdfa7..887695d94 100644 --- a/relay/server/peer.go +++ b/relay/server/peer.go @@ -27,6 +27,8 @@ type Peer struct { metrics *metrics.Metrics log *log.Entry id messages.PeerID + version messages.ProtocolVersion + enc messages.Encoder conn listener.Conn connMu sync.RWMutex store *store.Store @@ -42,12 +44,14 @@ type Peer struct { } // NewPeer creates a new Peer instance and prepare custom logging -func NewPeer(metrics *metrics.Metrics, id messages.PeerID, conn listener.Conn, store *store.Store, notifier *store.PeerNotifier) *Peer { +func NewPeer(metrics *metrics.Metrics, id messages.PeerID, version messages.ProtocolVersion, conn listener.Conn, store *store.Store, notifier *store.PeerNotifier) *Peer { ctx, cancel := context.WithCancel(context.Background()) p := &Peer{ metrics: metrics, log: log.WithField("peer_id", id.String()), id: id, + version: version, + enc: messages.NewEncoder(version), conn: conn, store: store, notifier: notifier, @@ -95,16 +99,15 @@ func (p *Peer) Work() { msg := buf[:n] - _, err = messages.ValidateVersion(msg) - if err != nil { - p.log.Warnf("failed to validate protocol version: %s", err) - return + if err := messages.ValidateConnectionVersion(msg, p.version); err != nil { + p.log.Tracef("drop message: %s", err) + continue } msgType, err := messages.DetermineClientMessageType(msg) if err != nil { - p.log.Errorf("failed to determine message type: %s", err) - return + p.log.Tracef("drop message: %s", err) + continue } p.handleMsgType(ctx, msgType, hc, n, msg) @@ -115,6 +118,11 @@ func (p *Peer) ID() messages.PeerID { return p.id } +// Version returns the protocol version selected for this connection +func (p *Peer) Version() messages.ProtocolVersion { + return p.version +} + func (p *Peer) handleMsgType(ctx context.Context, msgType messages.MsgType, hc *healthcheck.Sender, n int, msg []byte) { switch msgType { case messages.MsgTypeHealthCheck: @@ -133,7 +141,7 @@ func (p *Peer) handleMsgType(ctx context.Context, msgType messages.MsgType, hc * case messages.MsgTypeUnsubscribePeerState: p.handleUnsubscribePeerState(msg) default: - p.log.Warnf("received unexpected message type: %s", msgType) + p.log.Tracef("drop %s message: not allowed after authentication", msgType) } } @@ -149,7 +157,7 @@ func (p *Peer) Write(ctx context.Context, b []byte) (int, error) { func (p *Peer) CloseGracefully(ctx context.Context) { p.connMu.Lock() defer p.connMu.Unlock() - err := p.writeWithTimeout(ctx, messages.MarshalCloseMsg()) + err := p.writeWithTimeout(ctx, p.enc.CloseMsg()) if err != nil { p.log.Errorf("failed to send close message to peer: %s", p.String()) } @@ -187,7 +195,7 @@ func (p *Peer) handleHealthcheckEvents(ctx context.Context, hc *healthcheck.Send for { select { case <-hc.HealthCheck: - _, err := p.Write(ctx, messages.MarshalHealthcheck()) + _, err := p.Write(ctx, p.enc.Healthcheck()) if err != nil { p.log.Errorf("failed to send healthcheck message: %s", err) return @@ -220,7 +228,7 @@ func (p *Peer) handleTransportMsg(msg []byte) { } dp := item.(*Peer) - err = messages.UpdateTransportMsg(msg, p.id) + err = messages.UpdateTransportMsg(msg, dp.version, p.id) if err != nil { p.log.Errorf("failed to update transport message: %s", err) return @@ -267,7 +275,7 @@ func (p *Peer) handleUnsubscribePeerState(msg []byte) { } func (p *Peer) sendPeersOnline(peers []messages.PeerID) { - msgs, err := messages.MarshalPeersOnline(peers) + msgs, err := p.enc.PeersOnline(peers) if err != nil { p.log.Errorf("failed to marshal peer location message: %s", err) return @@ -284,7 +292,7 @@ func (p *Peer) sendPeersWentOffline(peers []messages.PeerID) { p.notificationMutex.Lock() defer p.notificationMutex.Unlock() - msgs, err := messages.MarshalPeersWentOffline(peers) + msgs, err := p.enc.PeersWentOffline(peers) if err != nil { p.log.Errorf("failed to marshal peer location message: %s", err) return diff --git a/relay/server/peer_test.go b/relay/server/peer_test.go new file mode 100644 index 000000000..3b6a8de41 --- /dev/null +++ b/relay/server/peer_test.go @@ -0,0 +1,99 @@ +package server + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/relay/auth/allow" + "github.com/netbirdio/netbird/shared/relay/messages" +) + +func TestPeer_DropsFramesItCannotHandle(t *testing.T) { + secondAuth, err := messages.MarshalAuthMsg(messages.HashID("alice"), []byte("token")) + require.NoError(t, err) + + cases := []struct { + name string + frame []byte + }{ + {"unknown message type", []byte{byte(messages.ProtocolV1), 200, 0xDE, 0xAD}}, + {"version above the selected one", []byte{2, byte(messages.MsgTypeHealthCheck)}}, + {"version zero", []byte{0, byte(messages.MsgTypeHealthCheck)}}, + {"header shorter than two bytes", []byte{byte(messages.ProtocolV1)}}, + {"reserved hello type", []byte{byte(messages.ProtocolV1), byte(messages.MsgTypeHello)}}, + {"server to client peers online type", []byte{byte(messages.ProtocolV1), byte(messages.MsgTypePeersOnline)}}, + {"auth after authentication", secondAuth}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + relay := newTestRelay(t, &allow.Auth{}) + alice, _ := acceptPeer(t, relay, "alice") + _, bobID := acceptPeer(t, relay, "bob") + + alice.deliver(t, tc.frame) + subscribe, err := messages.MarshalSubPeerStateMsg([]messages.PeerID{bobID}) + require.NoError(t, err) + alice.deliver(t, subscribe[0]) + + // The subscription answer proves the loop survived the dropped frame before it. + frames := alice.waitFrames(t, 2) + assert.False(t, alice.isClosed(), "a dropped frame must not close the connection") + online := frames[1] + assert.Equal(t, byte(messages.ProtocolV1), online[0], "notifications must carry the connection version") + assert.Equal(t, byte(messages.MsgTypePeersOnline), online[1], "the subscription must be answered") + ids, err := messages.UnmarshalPeersOnlineMsg(online) + require.NoError(t, err) + assert.Equal(t, []messages.PeerID{bobID}, ids, "the online peer must be reported") + }) + } +} + +func TestPeer_ClosesOnClientCloseMessage(t *testing.T) { + relay := newTestRelay(t, &allow.Auth{}) + conn, peerID := acceptPeer(t, relay, "alice") + + conn.deliver(t, messages.MarshalCloseMsg()) + + conn.waitClosed(t) + assert.Len(t, conn.frames(), 1, "only the auth response may be written") + assert.Eventually(t, func() bool { + _, ok := relay.store.Peer(peerID) + return !ok + }, testWaitTimeout, 5*time.Millisecond, "the peer must be removed from the store") +} + +func TestPeer_ForwardsTransportWithSenderAndDestinationVersion(t *testing.T) { + relay := newTestRelay(t, &allow.Auth{}) + alice, aliceID := acceptPeer(t, relay, "alice") + bob, bobID := acceptPeer(t, relay, "bob") + + payload := []byte("hello bob") + transport, err := messages.MarshalTransportMsg(bobID, payload) + require.NoError(t, err) + alice.deliver(t, transport) + + frames := bob.waitFrames(t, 2) + forwarded := frames[1] + assert.Equal(t, byte(messages.ProtocolV1), forwarded[0], "the forwarded frame must carry the destination's version") + assert.Equal(t, byte(messages.MsgTypeTransport), forwarded[1], "the forwarded frame must stay a transport message") + senderID, gotPayload, err := messages.UnmarshalTransportMsg(forwarded) + require.NoError(t, err) + assert.Equal(t, aliceID, *senderID, "the peer ID must be rewritten to the sender") + assert.Equal(t, payload, gotPayload, "the payload must be preserved") + assert.False(t, alice.isClosed(), "the sender must stay connected") +} + +func TestPeer_GracefulCloseCarriesConnectionVersion(t *testing.T) { + relay := newTestRelay(t, &allow.Auth{}) + conn, _ := acceptPeer(t, relay, "alice") + + relay.Shutdown(context.Background()) + + frames := conn.waitFrames(t, 2) + assert.Equal(t, messages.NewEncoder(messages.ProtocolV1).CloseMsg(), frames[1], "the close message must carry the connection version") + assert.True(t, conn.isClosed(), "the connection must be closed after the close message") +} diff --git a/relay/server/relay.go b/relay/server/relay.go index 58f9db840..bd580efd0 100644 --- a/relay/server/relay.go +++ b/relay/server/relay.go @@ -150,8 +150,17 @@ func (r *Relay) Accept(conn listener.Conn) { return } - peer := NewPeer(r.metrics, *peerID, conn, r.store, r.notifier) - peer.log.Infof("peer connected from: %s", conn.RemoteAddr()) + if err := h.handshakeResponse(hsCtx); err != nil { + log.Errorf("failed to send handshake response, close connection: %s", err) + if cErr := conn.Close(); cErr != nil { + log.Errorf("failed to close connection, %s: %s", conn.RemoteAddr(), cErr) + } + return + } + r.metrics.RecordAuthenticationTime(time.Since(acceptTime)) + + peer := NewPeer(r.metrics, *peerID, h.selectedVersion, conn, r.store, r.notifier) + peer.log.Infof("peer connected from: %s, advertised protocol version: %d, selected: %d", conn.RemoteAddr(), h.advertisedVersion, h.selectedVersion) storeTime := time.Now() if isReconnection := r.store.AddPeer(peer); isReconnection { r.metrics.RecordPeerReconnection() @@ -161,6 +170,7 @@ func (r *Relay) Accept(conn listener.Conn) { transport := conn.Protocol() r.metrics.RecordPeerStoreTime(time.Since(storeTime)) r.metrics.PeerConnected(peer.String(), transport) + r.metrics.RecordProtocolVersion(h.advertisedVersion, h.selectedVersion) go func() { peer.Work() if deleted := r.store.DeletePeer(peer); deleted { @@ -169,12 +179,6 @@ func (r *Relay) Accept(conn listener.Conn) { peer.log.Debugf("relay connection closed") r.metrics.PeerDisconnected(peer.String(), transport) }() - - if err := h.handshakeResponse(hsCtx); err != nil { - log.Errorf("failed to send handshake response, close peer: %s", err) - peer.Close() - } - r.metrics.RecordAuthenticationTime(time.Since(acceptTime)) } // Shutdown closes the relay server diff --git a/relay/server/ws_version_test.go b/relay/server/ws_version_test.go new file mode 100644 index 000000000..bd5fa7303 --- /dev/null +++ b/relay/server/ws_version_test.go @@ -0,0 +1,97 @@ +package server + +import ( + "context" + "testing" + "time" + + "github.com/coder/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/relay" + "github.com/netbirdio/netbird/shared/relay/messages" +) + +func TestServer_FutureClientOverWebSocket(t *testing.T) { + addr := freeAddress(t) + srv := newTestServer(t, addr) + errChan := make(chan error, 1) + go func() { + errChan <- srv.Listen(ListenerConfig{Address: addr}) + }() + waitForListeners(t, srv, errChan) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + instanceURL := srv.InstanceURL() + want, err := messages.MarshalAuthResponse(instanceURL.String()) + require.NoError(t, err) + + // A synthetic client advertising v2 with an extension area that a v1 server does not read. + future := dialRelayWebSocket(t, ctx, addr) + futureID := messages.HashID("future-client") + futureAuth, err := messages.MarshalAuthEnvelope(2, futureID, []byte("future-token"), []byte(`{"cred2":"value"}`)) + require.NoError(t, err) + require.NoError(t, future.Write(ctx, websocket.MessageBinary, futureAuth)) + assert.Equal(t, want, readFrame(t, ctx, future), "a v2 client must receive the v1 response") + + legacy := dialRelayWebSocket(t, ctx, addr) + legacyID := messages.HashID("legacy-client") + legacyAuth, err := messages.MarshalAuthMsg(legacyID, []byte("legacy-token")) + require.NoError(t, err) + require.NoError(t, legacy.Write(ctx, websocket.MessageBinary, legacyAuth)) + assert.Equal(t, want, readFrame(t, ctx, legacy), "a v1 client must receive the response it gets today") + + toLegacy, err := messages.MarshalTransportMsg(legacyID, []byte("from future")) + require.NoError(t, err) + require.NoError(t, future.Write(ctx, websocket.MessageBinary, toLegacy)) + assertTransportFrame(t, readFrame(t, ctx, legacy), futureID, "from future") + + toFuture, err := messages.MarshalTransportMsg(futureID, []byte("from legacy")) + require.NoError(t, err) + require.NoError(t, legacy.Write(ctx, websocket.MessageBinary, toFuture)) + assertTransportFrame(t, readFrame(t, ctx, future), legacyID, "from legacy") + + // After the response the connection is v1. A frame with another version is dropped and the + // connection stays usable, which the transport message right after it proves. + require.NoError(t, future.Write(ctx, websocket.MessageBinary, []byte{2, byte(messages.MsgTypeHealthCheck)})) + toLegacyAgain, err := messages.MarshalTransportMsg(legacyID, []byte("after the dropped frame")) + require.NoError(t, err) + require.NoError(t, future.Write(ctx, websocket.MessageBinary, toLegacyAgain)) + assertTransportFrame(t, readFrame(t, ctx, legacy), futureID, "after the dropped frame") + + require.NoError(t, future.Write(ctx, websocket.MessageBinary, messages.MarshalCloseMsg())) + require.NoError(t, legacy.Write(ctx, websocket.MessageBinary, messages.MarshalCloseMsg())) +} + +func dialRelayWebSocket(t *testing.T, ctx context.Context, addr string) *websocket.Conn { + t.Helper() + conn, resp, err := websocket.Dial(ctx, "ws://"+addr+relay.WebSocketURLPath, nil) //nolint:bodyclose + require.NoError(t, err) + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + t.Cleanup(func() { _ = conn.CloseNow() }) + return conn +} + +func readFrame(t *testing.T, ctx context.Context, conn *websocket.Conn) []byte { + t.Helper() + msgType, frame, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, websocket.MessageBinary, msgType, "relay frames are binary messages") + return frame +} + +func assertTransportFrame(t *testing.T, frame []byte, sender messages.PeerID, payload string) { + t.Helper() + require.GreaterOrEqual(t, len(frame), 2, "a frame must carry the protocol header") + assert.Equal(t, byte(messages.ProtocolV1), frame[0], "the forwarded frame must carry the destination's version") + assert.Equal(t, byte(messages.MsgTypeTransport), frame[1], "the forwarded frame must be a transport message") + id, got, err := messages.UnmarshalTransportMsg(frame) + require.NoError(t, err) + assert.Equal(t, sender, *id, "the peer ID must be rewritten to the sender") + assert.Equal(t, payload, string(got), "the payload must be preserved") +} diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index ca8c1339b..f129ada26 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -3961,6 +3961,11 @@ components: type: integer description: The devices last sync requirement interval in hours. Minimum value is 24 hours. minimum: 24 + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + minimum: 1 + maximum: 1440 enabled: type: boolean description: Indicates whether the integration is enabled @@ -3979,6 +3984,7 @@ components: - tenant_id - groups - last_synced_interval + - sync_interval - enabled properties: id: @@ -4025,6 +4031,9 @@ components: last_synced_interval: type: integer description: The devices last sync requirement interval in hours. + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. enabled: type: boolean description: Indicates whether the integration is enabled @@ -4047,6 +4056,11 @@ components: type: integer description: The devices last sync requirement interval in hours. Minimum value is 24 hours. minimum: 24 + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + minimum: 1 + maximum: 1440 enabled: type: boolean description: Indicates whether the integration is enabled @@ -4073,6 +4087,7 @@ components: - groups - last_synced_interval - match_attributes + - sync_interval - enabled properties: id: @@ -4115,6 +4130,9 @@ components: description: The devices last sync requirement interval in hours. match_attributes: $ref: '#/components/schemas/SentinelOneMatchAttributes' + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. enabled: type: boolean description: Indicates whether the integration is enabled @@ -4174,6 +4192,11 @@ components: minimum: 0 maximum: 100 example: 75 + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + minimum: 1 + maximum: 1440 enabled: type: boolean description: Indicates whether the integration is enabled @@ -4197,6 +4220,7 @@ components: - cloud_id - groups - zta_score_threshold + - sync_interval - enabled properties: id: @@ -4237,6 +4261,9 @@ components: zta_score_threshold: type: integer description: The minimum Zero Trust Assessment score required for agent approval (0-100) + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. enabled: type: boolean description: Indicates whether the integration is enabled @@ -4260,6 +4287,11 @@ components: type: integer description: The devices last sync requirement interval in hours. Minimum value is 24 hours minimum: 24 + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + minimum: 1 + maximum: 1440 enabled: type: boolean description: Indicates whether the integration is enabled @@ -4285,6 +4317,7 @@ components: - groups - last_synced_interval - match_attributes + - sync_interval - enabled properties: id: @@ -4322,6 +4355,9 @@ components: last_synced_interval: type: integer description: The devices last sync requirement interval in hours. + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. enabled: type: boolean description: Indicates whether the integration is enabled @@ -4370,6 +4406,11 @@ components: type: integer description: The devices last sync requirement interval in hours. Minimum value is 24 hours minimum: 24 + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + minimum: 1 + maximum: 1440 enabled: type: boolean description: Indicates whether the integration is enabled @@ -4396,6 +4437,7 @@ components: - groups - last_synced_interval - match_attributes + - sync_interval - enabled properties: id: @@ -4436,6 +4478,9 @@ components: last_synced_interval: type: integer description: The devices last sync requirement interval in hours. + sync_interval: + type: integer + description: How often devices are synced from the integration, in minutes. enabled: type: boolean description: Indicates whether the integration is enabled diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 1595553ed..a5ffe4145 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -2918,6 +2918,9 @@ type EDRFalconRequest struct { // Secret CrowdStrike API client secret Secret string `json:"secret"` + // SyncInterval How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + SyncInterval *int `json:"sync_interval,omitempty"` + // ZtaScoreThreshold The minimum Zero Trust Assessment score required for agent approval (0-100) ZtaScoreThreshold int `json:"zta_score_threshold"` } @@ -2948,6 +2951,9 @@ type EDRFalconResponse struct { // LastSyncedAt Timestamp of when the integration was last synced. LastSyncedAt time.Time `json:"last_synced_at"` + // SyncInterval How often devices are synced from the integration, in minutes. + SyncInterval int `json:"sync_interval"` + // UpdatedAt Timestamp of when the integration was last updated. UpdatedAt time.Time `json:"updated_at"` @@ -2974,6 +2980,9 @@ type EDRFleetDMRequest struct { // MatchAttributes Attribute conditions to match when approving FleetDM hosts. Most attributes work with FleetDM's free/open source version. Premium-only attributes are marked accordingly MatchAttributes FleetDMMatchAttributes `json:"match_attributes"` + + // SyncInterval How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + SyncInterval *int `json:"sync_interval,omitempty"` } // EDRFleetDMResponse Represents a FleetDM EDR integration configuration @@ -3008,6 +3017,9 @@ type EDRFleetDMResponse struct { // MatchAttributes Attribute conditions to match when approving FleetDM hosts. Most attributes work with FleetDM's free/open source version. Premium-only attributes are marked accordingly MatchAttributes FleetDMMatchAttributes `json:"match_attributes"` + // SyncInterval How often devices are synced from the integration, in minutes. + SyncInterval int `json:"sync_interval"` + // UpdatedAt Timestamp of when the integration was last updated. UpdatedAt time.Time `json:"updated_at"` } @@ -3031,6 +3043,9 @@ type EDRHuntressRequest struct { // MatchAttributes Attribute conditions to match when approving agents MatchAttributes HuntressMatchAttributes `json:"match_attributes"` + + // SyncInterval How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + SyncInterval *int `json:"sync_interval,omitempty"` } // EDRHuntressResponse Represents a Huntress EDR integration configuration @@ -3062,6 +3077,9 @@ type EDRHuntressResponse struct { // MatchAttributes Attribute conditions to match when approving agents MatchAttributes HuntressMatchAttributes `json:"match_attributes"` + // SyncInterval How often devices are synced from the integration, in minutes. + SyncInterval int `json:"sync_interval"` + // UpdatedAt Timestamp of when the integration was last updated. UpdatedAt time.Time `json:"updated_at"` } @@ -3083,6 +3101,9 @@ type EDRIntuneRequest struct { // Secret The Azure application client secret Secret string `json:"secret"` + // SyncInterval How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + SyncInterval *int `json:"sync_interval,omitempty"` + // TenantId The Azure tenant id TenantId string `json:"tenant_id"` } @@ -3116,6 +3137,9 @@ type EDRIntuneResponse struct { // LastSyncedInterval The devices last sync requirement interval in hours. LastSyncedInterval int `json:"last_synced_interval"` + // SyncInterval How often devices are synced from the integration, in minutes. + SyncInterval int `json:"sync_interval"` + // TenantId The Azure tenant id TenantId string `json:"tenant_id"` @@ -3142,6 +3166,9 @@ type EDRSentinelOneRequest struct { // MatchAttributes Attribute conditions to match when approving agents MatchAttributes SentinelOneMatchAttributes `json:"match_attributes"` + + // SyncInterval How often devices are synced from the integration, in minutes. Minimum value is 1 minute, maximum is 1440 minutes. + SyncInterval *int `json:"sync_interval,omitempty"` } // EDRSentinelOneResponse Represents a SentinelOne EDR integration configuration @@ -3176,6 +3203,9 @@ type EDRSentinelOneResponse struct { // MatchAttributes Attribute conditions to match when approving agents MatchAttributes SentinelOneMatchAttributes `json:"match_attributes"` + // SyncInterval How often devices are synced from the integration, in minutes. + SyncInterval int `json:"sync_interval"` + // UpdatedAt Timestamp of when the integration was last updated. UpdatedAt time.Time `json:"updated_at"` } diff --git a/shared/management/networkmap/envelope_test.go b/shared/management/networkmap/envelope_test.go index 98333a3c8..fe71016b8 100644 --- a/shared/management/networkmap/envelope_test.go +++ b/shared/management/networkmap/envelope_test.go @@ -244,6 +244,7 @@ func TestEnvelopeToNetworkMap_EmptyComponents(t *testing.T) { }) envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{ + Peer: &nmdata.Peer{ID: "peer-A", Key: localPeerKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 1})}, Components: c, DNSDomain: "netbird.cloud", }) diff --git a/shared/relay/messages/message.go b/shared/relay/messages/message.go index fdbf2a6e7..e25571edd 100644 --- a/shared/relay/messages/message.go +++ b/shared/relay/messages/message.go @@ -2,16 +2,27 @@ package messages import ( "bytes" + "encoding/binary" "errors" "fmt" + "math" ) const ( - MaxHandshakeSize = 212 + // TODO: MaxHandshakeSize is the buffer size of the first read on every connection before + // authentication. Weigh it against memory use under many unauthenticated connections; a + // smaller limit may still fit the extension area. + MaxHandshakeSize = 8192 MaxHandshakeRespSize = 8192 MaxMessageSize = 8820 - CurrentProtocolVersion = 1 + ProtocolV1 ProtocolVersion = 1 + // CurrentProtocolVersion is the highest protocol version this build supports. + CurrentProtocolVersion = ProtocolV1 + + // authEnvelopeMinVersion is the first advertised version whose Auth message carries a + // length-prefixed token followed by an extension area. + authEnvelopeMinVersion ProtocolVersion = 2 MsgTypeUnknown MsgType = 0 // MsgTypeHello and MsgTypeHelloResponse are the removed legacy handshake @@ -38,10 +49,13 @@ const ( // auth message sizeOfMagicByte = 4 + sizeOfTokenLen = 2 headerSizeAuth = sizeOfMagicByte + peerIDSize offsetMagicByte = sizeOfProtoHeader offsetAuthPeerID = sizeOfProtoHeader + sizeOfMagicByte headerTotalSizeAuth = sizeOfProtoHeader + headerSizeAuth + offsetAuthTokenLen = headerTotalSizeAuth + offsetAuthToken = offsetAuthTokenLen + sizeOfTokenLen // transport headerSizeTransport = peerIDSize @@ -52,6 +66,10 @@ const ( var ( ErrInvalidMessageLength = errors.New("invalid message length") ErrUnsupportedVersion = errors.New("unsupported version") + ErrInvalidTokenLength = errors.New("invalid token length") + ErrReservedMsgType = errors.New("reserved message type") + ErrMsgTypeDirection = errors.New("message type not valid from client") + ErrUnknownMsgType = errors.New("unknown message type") magicHeader = []byte{0x21, 0x12, 0xA4, 0x42} @@ -60,6 +78,19 @@ var ( type MsgType byte +// ProtocolVersion is the relay wire protocol version carried in the first byte of every frame. +type ProtocolVersion uint8 + +// Encoder marshals server-originated messages with the protocol version selected for one connection. +type Encoder struct { + version ProtocolVersion +} + +// NewEncoder returns an Encoder that stamps every message with the given protocol version. +func NewEncoder(version ProtocolVersion) Encoder { + return Encoder{version: version} +} + func (m MsgType) String() string { switch m { case MsgTypeHello: @@ -89,18 +120,99 @@ func (m MsgType) String() string { } } -// ValidateVersion checks if the given version is supported by the protocol -func ValidateVersion(msg []byte) (int, error) { +// Version returns the protocol version the Encoder stamps on its messages. +func (e Encoder) Version() ProtocolVersion { + return e.version +} + +// AuthResponse creates the response to a successful Auth message. The payload is the instance URL +// of the server, which the clients use to choose a common Relay server. +func (e Encoder) AuthResponse(address string) ([]byte, error) { + ab := []byte(address) + msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+len(ab)) + + msg[0] = byte(e.version) + msg[1] = byte(MsgTypeAuthResponse) + + msg = append(msg, ab...) + + if len(msg) > MaxHandshakeRespSize { + return nil, fmt.Errorf("invalid message length: %d", len(msg)) + } + + return msg, nil +} + +// CloseMsg creates a close message for a graceful disconnect. +func (e Encoder) CloseMsg() []byte { + return []byte{ + byte(e.version), + byte(MsgTypeClose), + } +} + +// Healthcheck creates a health check message. +func (e Encoder) Healthcheck() []byte { + return []byte{ + byte(e.version), + byte(MsgTypeHealthCheck), + } +} + +// PeersOnline creates the peers online notification messages. +func (e Encoder) PeersOnline(ids []PeerID) ([][]byte, error) { + return marshalPeerIDs(e.version, ids, byte(MsgTypePeersOnline)) +} + +// PeersWentOffline creates the peers went offline notification messages. +func (e Encoder) PeersWentOffline(ids []PeerID) ([][]byte, error) { + return marshalPeerIDs(e.version, ids, byte(MsgTypePeersWentOffline)) +} + +// ReadVersion returns the protocol version carried in the frame header. Zero is invalid. +func ReadVersion(msg []byte) (ProtocolVersion, error) { if len(msg) < sizeOfProtoHeader { return 0, ErrInvalidMessageLength } - version := int(msg[0]) - if version != CurrentProtocolVersion { + version := ProtocolVersion(msg[0]) + if version == 0 { return 0, fmt.Errorf("%d: %w", version, ErrUnsupportedVersion) } return version, nil } +// SelectVersion returns the highest protocol version both sides support. +func SelectVersion(advertised, supported ProtocolVersion) ProtocolVersion { + if advertised < supported { + return advertised + } + return supported +} + +// ValidateVersion checks that the frame carries the protocol version of this build. +func ValidateVersion(msg []byte) (int, error) { + version, err := ReadVersion(msg) + if err != nil { + return 0, err + } + if version != CurrentProtocolVersion { + return 0, fmt.Errorf("%d: %w", version, ErrUnsupportedVersion) + } + return int(version), nil +} + +// ValidateConnectionVersion checks that the frame carries the version selected for the connection. +func ValidateConnectionVersion(msg []byte, selected ProtocolVersion) error { + version, err := ReadVersion(msg) + if err != nil { + return err + } + if version != selected { + return fmt.Errorf("%d instead of selected %d: %w", version, selected, ErrUnsupportedVersion) + } + return nil +} + // DetermineClientMessageType determines the message type from the first the message func DetermineClientMessageType(msg []byte) (MsgType, error) { if len(msg) < sizeOfProtoHeader { @@ -117,8 +229,12 @@ func DetermineClientMessageType(msg []byte) (MsgType, error) { MsgTypeSubscribePeerState, MsgTypeUnsubscribePeerState: return msgType, nil + case MsgTypeHello, MsgTypeHelloResponse: + return MsgTypeUnknown, fmt.Errorf("%d: %w", msgType, ErrReservedMsgType) + case MsgTypeAuthResponse, MsgTypePeersOnline, MsgTypePeersWentOffline: + return MsgTypeUnknown, fmt.Errorf("%d: %w", msgType, ErrMsgTypeDirection) default: - return MsgTypeUnknown, fmt.Errorf("invalid msg type %d", msgType) + return MsgTypeUnknown, fmt.Errorf("%d: %w", msgType, ErrUnknownMsgType) } } @@ -162,8 +278,41 @@ func MarshalAuthMsg(peerID PeerID, authPayload []byte) ([]byte, error) { return msg, nil } -// UnmarshalAuthMsg extracts peerID and the auth payload from the message +// MarshalAuthEnvelope creates the Auth message layout used by clients advertising protocol +// version 2 or higher: the token is length-prefixed and followed by an extension area that +// servers without support for it ignore. +func MarshalAuthEnvelope(version ProtocolVersion, peerID PeerID, token, ext []byte) ([]byte, error) { + if version < authEnvelopeMinVersion { + return nil, fmt.Errorf("protocol version %d does not use the auth envelope", version) + } + if len(token) == 0 || len(token) > math.MaxUint16 { + return nil, fmt.Errorf("%d: %w", len(token), ErrInvalidTokenLength) + } + + size := offsetAuthToken + len(token) + len(ext) + if size > MaxHandshakeSize { + return nil, fmt.Errorf("too large auth message: %d", size) + } + + msg := make([]byte, size) + msg[0] = byte(version) + msg[1] = byte(MsgTypeAuth) + copy(msg[offsetMagicByte:], magicHeader) + copy(msg[offsetAuthPeerID:], peerID[:]) + binary.BigEndian.PutUint16(msg[offsetAuthTokenLen:], uint16(len(token))) + copy(msg[offsetAuthToken:], token) + copy(msg[offsetAuthToken+len(token):], ext) + return msg, nil +} + +// UnmarshalAuthMsg extracts the peer ID and the auth token from an Auth message of any +// protocol version. The extension area of the envelope layout is not returned. func UnmarshalAuthMsg(msg []byte) (*PeerID, []byte, error) { + version, err := ReadVersion(msg) + if err != nil { + return nil, nil, err + } + if len(msg) < headerTotalSizeAuth { return nil, nil, ErrInvalidMessageLength } @@ -174,7 +323,15 @@ func UnmarshalAuthMsg(msg []byte) (*PeerID, []byte, error) { } peerID := PeerID(msg[offsetAuthPeerID:headerTotalSizeAuth]) - return &peerID, msg[headerTotalSizeAuth:], nil + if version < authEnvelopeMinVersion { + return &peerID, msg[headerTotalSizeAuth:], nil + } + + token, err := unmarshalAuthEnvelopeToken(msg) + if err != nil { + return nil, nil, err + } + return &peerID, token, nil } // MarshalAuthResponse creates a response message to the auth. @@ -182,19 +339,7 @@ func UnmarshalAuthMsg(msg []byte) (*PeerID, []byte, error) { // instance URL. This URL will be used by choose the common Relay server in case if the peers are in different Relay // servers. func MarshalAuthResponse(address string) ([]byte, error) { - ab := []byte(address) - msg := make([]byte, sizeOfProtoHeader, sizeOfProtoHeader+len(ab)) - - msg[0] = byte(CurrentProtocolVersion) - msg[1] = byte(MsgTypeAuthResponse) - - msg = append(msg, ab...) - - if len(msg) > MaxHandshakeRespSize { - return nil, fmt.Errorf("invalid message length: %d", len(msg)) - } - - return msg, nil + return NewEncoder(CurrentProtocolVersion).AuthResponse(address) } // UnmarshalAuthResponse it is a confirmation message to auth success @@ -209,10 +354,7 @@ func UnmarshalAuthResponse(msg []byte) (string, error) { // The close message is used to close the connection gracefully between the client and the server. The server and the // client can send this message. After receiving this message, the server or client will close the connection. func MarshalCloseMsg() []byte { - return []byte{ - byte(CurrentProtocolVersion), - byte(MsgTypeClose), - } + return NewEncoder(CurrentProtocolVersion).CloseMsg() } // MarshalTransportMsg creates a transport message. @@ -252,13 +394,14 @@ func UnmarshalTransportID(buf []byte) (*PeerID, error) { return &id, nil } -// UpdateTransportMsg updates the peerID in the transport message. -// With this function the server can reuse the given byte slice to update the peerID in the transport message. So do -// need to allocate a new byte slice. -func UpdateTransportMsg(msg []byte, peerID PeerID) error { +// UpdateTransportMsg rewrites the transport message in place for forwarding: the header carries +// the protocol version of the destination connection and the peer ID field the sender's ID. +// The payload bytes are left untouched, so no new byte slice is allocated. +func UpdateTransportMsg(msg []byte, version ProtocolVersion, peerID PeerID) error { if len(msg) < offsetTransportID+peerIDSize { return ErrInvalidMessageLength } + msg[0] = byte(version) copy(msg[offsetTransportID:], peerID[:]) return nil } @@ -269,3 +412,20 @@ func UpdateTransportMsg(msg []byte, peerID PeerID) error { func MarshalHealthcheck() []byte { return healthCheckMsg } + +func unmarshalAuthEnvelopeToken(msg []byte) ([]byte, error) { + if len(msg) < offsetAuthToken { + return nil, ErrInvalidMessageLength + } + + tokenLen := int(binary.BigEndian.Uint16(msg[offsetAuthTokenLen:offsetAuthToken])) + if tokenLen == 0 { + return nil, fmt.Errorf("%d: %w", tokenLen, ErrInvalidTokenLength) + } + + end := offsetAuthToken + tokenLen + if end > len(msg) { + return nil, fmt.Errorf("%d exceeds the message: %w", tokenLen, ErrInvalidTokenLength) + } + return msg[offsetAuthToken:end], nil +} diff --git a/shared/relay/messages/message_test.go b/shared/relay/messages/message_test.go index 26a504063..b3727c86a 100644 --- a/shared/relay/messages/message_test.go +++ b/shared/relay/messages/message_test.go @@ -1,7 +1,11 @@ package messages import ( + "encoding/binary" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDetermineClientMessageTypeRejectsHello(t *testing.T) { @@ -119,3 +123,245 @@ func TestMarshalHealthcheck(t *testing.T) { t.Errorf("expected %d, got %d", MsgTypeHealthCheck, msgType) } } + +func TestReadVersion(t *testing.T) { + _, err := ReadVersion([]byte{byte(ProtocolV1)}) + assert.ErrorIs(t, err, ErrInvalidMessageLength, "a header needs two bytes") + + _, err = ReadVersion([]byte{0, byte(MsgTypeAuth)}) + assert.ErrorIs(t, err, ErrUnsupportedVersion, "version zero is invalid") + + for _, version := range []ProtocolVersion{1, 2, 128, 255} { + got, err := ReadVersion([]byte{byte(version), byte(MsgTypeAuth)}) + require.NoError(t, err) + assert.Equal(t, version, got, "the advertised version must be returned unchanged") + } +} + +func TestSelectVersion(t *testing.T) { + assert.Equal(t, ProtocolV1, SelectVersion(1, 1), "equal versions select themselves") + assert.Equal(t, ProtocolV1, SelectVersion(2, 1), "a higher advertised version selects the server maximum") + assert.Equal(t, ProtocolV1, SelectVersion(255, 1), "the highest advertised version selects the server maximum") + assert.Equal(t, ProtocolVersion(2), SelectVersion(2, 3), "a lower advertised version selects itself") + assert.Equal(t, ProtocolVersion(2), SelectVersion(3, 2), "a higher advertised version selects the server maximum") +} + +func TestValidateVersionAcceptsOnlyCurrent(t *testing.T) { + _, err := ValidateVersion([]byte{byte(CurrentProtocolVersion), byte(MsgTypeHealthCheck)}) + require.NoError(t, err) + + _, err = ValidateVersion([]byte{byte(CurrentProtocolVersion) + 1, byte(MsgTypeHealthCheck)}) + assert.ErrorIs(t, err, ErrUnsupportedVersion, "the client build accepts only its own version") + + _, err = ValidateVersion([]byte{0, byte(MsgTypeHealthCheck)}) + assert.ErrorIs(t, err, ErrUnsupportedVersion, "version zero is invalid") +} + +func TestValidateConnectionVersion(t *testing.T) { + require.NoError(t, ValidateConnectionVersion([]byte{1, byte(MsgTypeHealthCheck)}, ProtocolV1)) + assert.ErrorIs(t, ValidateConnectionVersion([]byte{2, byte(MsgTypeHealthCheck)}, ProtocolV1), ErrUnsupportedVersion, "a frame must carry the selected version") + assert.ErrorIs(t, ValidateConnectionVersion([]byte{1, byte(MsgTypeHealthCheck)}, 2), ErrUnsupportedVersion, "a frame below the selected version is rejected too") + assert.ErrorIs(t, ValidateConnectionVersion([]byte{0, byte(MsgTypeHealthCheck)}, ProtocolV1), ErrUnsupportedVersion, "version zero is invalid") + assert.ErrorIs(t, ValidateConnectionVersion([]byte{1}, ProtocolV1), ErrInvalidMessageLength, "a header needs two bytes") +} + +func TestDetermineClientMessageTypeClassification(t *testing.T) { + rejected := []struct { + name string + msgType MsgType + wantErr error + }{ + {"hello is reserved", MsgTypeHello, ErrReservedMsgType}, + {"hello response is reserved", MsgTypeHelloResponse, ErrReservedMsgType}, + {"auth response is server to client only", MsgTypeAuthResponse, ErrMsgTypeDirection}, + {"peers online is server to client only", MsgTypePeersOnline, ErrMsgTypeDirection}, + {"peers went offline is server to client only", MsgTypePeersWentOffline, ErrMsgTypeDirection}, + {"zero is unknown", MsgTypeUnknown, ErrUnknownMsgType}, + {"a future type is unknown", MsgType(200), ErrUnknownMsgType}, + } + for _, tc := range rejected { + t.Run(tc.name, func(t *testing.T) { + got, err := DetermineClientMessageType([]byte{byte(ProtocolV1), byte(tc.msgType)}) + assert.ErrorIs(t, err, tc.wantErr) + assert.Equal(t, MsgTypeUnknown, got, "a rejected type must not be returned") + }) + } + + accepted := []MsgType{MsgTypeAuth, MsgTypeTransport, MsgTypeClose, MsgTypeHealthCheck, MsgTypeSubscribePeerState, MsgTypeUnsubscribePeerState} + for _, msgType := range accepted { + got, err := DetermineClientMessageType([]byte{byte(ProtocolV1), byte(msgType)}) + require.NoError(t, err) + assert.Equal(t, msgType, got, "a client message type must be returned") + } +} + +func TestAuthEnvelopeRoundTrip(t *testing.T) { + peerID := HashID("future-client") + token := []byte("0123456789abcdef") + cases := []struct { + name string + version ProtocolVersion + ext []byte + }{ + {"v2 without extension", 2, nil}, + {"v2 with json extension", 2, []byte(`{"cred2":"value"}`)}, + {"v255 with opaque extension", 255, []byte("opaque-extension-bytes")}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + msg, err := MarshalAuthEnvelope(tc.version, peerID, token, tc.ext) + require.NoError(t, err) + + version, err := ReadVersion(msg) + require.NoError(t, err) + assert.Equal(t, tc.version, version, "the header must carry the advertised version") + + msgType, err := DetermineClientMessageType(msg) + require.NoError(t, err) + assert.Equal(t, MsgType(MsgTypeAuth), msgType, "the envelope is an auth message") + + gotID, gotToken, err := UnmarshalAuthMsg(msg) + require.NoError(t, err) + assert.Equal(t, peerID, *gotID, "the peer ID must be preserved") + assert.Equal(t, token, gotToken, "the token must be returned without the extension bytes") + }) + } +} + +func TestMarshalAuthEnvelopeRejectsInvalidInput(t *testing.T) { + peerID := HashID("future-client") + + _, err := MarshalAuthEnvelope(ProtocolV1, peerID, []byte("token"), nil) + assert.Error(t, err, "v1 does not use the envelope layout") + + _, err = MarshalAuthEnvelope(2, peerID, nil, nil) + assert.ErrorIs(t, err, ErrInvalidTokenLength, "the token is mandatory") + + _, err = MarshalAuthEnvelope(2, peerID, []byte("token"), make([]byte, MaxHandshakeSize)) + assert.Error(t, err, "the envelope must fit the handshake size limit") +} + +func TestUnmarshalAuthMsgV1LayoutUnchanged(t *testing.T) { + peerID := HashID("legacy-client") + // In the v1 layout the token runs to the end of the frame, so bytes that would be a + // length prefix in the envelope belong to the token here. + payload := []byte{0x00, 0x05, 'a', 'b', 'c', 'd', 'e', 'f', 'g'} + msg, err := MarshalAuthMsg(peerID, payload) + require.NoError(t, err) + + gotID, gotToken, err := UnmarshalAuthMsg(msg) + require.NoError(t, err) + assert.Equal(t, peerID, *gotID, "the peer ID must be preserved") + assert.Equal(t, payload, gotToken, "the v1 token must be returned whole") +} + +func TestUnmarshalAuthMsgRejectsMalformedFrames(t *testing.T) { + peerID := HashID("future-client") + valid, err := MarshalAuthEnvelope(2, peerID, []byte("token"), []byte("ext")) + require.NoError(t, err) + + t.Run("zero token length", func(t *testing.T) { + msg := append([]byte(nil), valid...) + binary.BigEndian.PutUint16(msg[offsetAuthTokenLen:], 0) + _, _, err := UnmarshalAuthMsg(msg) + assert.ErrorIs(t, err, ErrInvalidTokenLength) + }) + + t.Run("token length past the end", func(t *testing.T) { + msg := append([]byte(nil), valid...) + binary.BigEndian.PutUint16(msg[offsetAuthTokenLen:], uint16(len(msg))) + _, _, err := UnmarshalAuthMsg(msg) + assert.ErrorIs(t, err, ErrInvalidTokenLength) + }) + + t.Run("truncated before the length field", func(t *testing.T) { + _, _, err := UnmarshalAuthMsg(valid[:headerTotalSizeAuth+1]) + assert.ErrorIs(t, err, ErrInvalidMessageLength) + }) + + t.Run("version zero", func(t *testing.T) { + msg := append([]byte(nil), valid...) + msg[0] = 0 + _, _, err := UnmarshalAuthMsg(msg) + assert.ErrorIs(t, err, ErrUnsupportedVersion) + }) + + t.Run("invalid magic", func(t *testing.T) { + msg := append([]byte(nil), valid...) + msg[offsetMagicByte] ^= 0xFF + _, _, err := UnmarshalAuthMsg(msg) + assert.Error(t, err) + }) + + t.Run("shorter than the fixed header", func(t *testing.T) { + _, _, err := UnmarshalAuthMsg(valid[:headerTotalSizeAuth-1]) + assert.ErrorIs(t, err, ErrInvalidMessageLength) + }) +} + +func TestEncoderStampsVersion(t *testing.T) { + for _, version := range []ProtocolVersion{1, 2, 7} { + enc := NewEncoder(version) + assert.Equal(t, version, enc.Version()) + assert.Equal(t, []byte{byte(version), byte(MsgTypeHealthCheck)}, enc.Healthcheck(), "healthcheck must carry the version") + assert.Equal(t, []byte{byte(version), byte(MsgTypeClose)}, enc.CloseMsg(), "close must carry the version") + + resp, err := enc.AuthResponse("rel://relay.example") + require.NoError(t, err) + assert.Equal(t, byte(version), resp[0], "auth response must carry the version") + assert.Equal(t, byte(MsgTypeAuthResponse), resp[1]) + addr, err := UnmarshalAuthResponse(resp) + require.NoError(t, err) + assert.Equal(t, "rel://relay.example", addr, "the auth response payload must be the address") + + online, err := enc.PeersOnline([]PeerID{HashID("a")}) + require.NoError(t, err) + require.Len(t, online, 1) + assert.Equal(t, byte(version), online[0][0], "peers online must carry the version") + assert.Equal(t, byte(MsgTypePeersOnline), online[0][1]) + + offline, err := enc.PeersWentOffline([]PeerID{HashID("a")}) + require.NoError(t, err) + require.Len(t, offline, 1) + assert.Equal(t, byte(version), offline[0][0], "peers went offline must carry the version") + assert.Equal(t, byte(MsgTypePeersWentOffline), offline[0][1]) + } +} + +func TestPackageEncodersUseCurrentVersion(t *testing.T) { + current := NewEncoder(CurrentProtocolVersion) + assert.Equal(t, current.CloseMsg(), MarshalCloseMsg()) + assert.Equal(t, current.Healthcheck(), MarshalHealthcheck()) + + want, err := current.AuthResponse("addr") + require.NoError(t, err) + got, err := MarshalAuthResponse("addr") + require.NoError(t, err) + assert.Equal(t, want, got) + + ids := []PeerID{HashID("a"), HashID("b")} + wantOnline, err := current.PeersOnline(ids) + require.NoError(t, err) + gotOnline, err := MarshalPeersOnline(ids) + require.NoError(t, err) + assert.Equal(t, wantOnline, gotOnline) +} + +func TestUpdateTransportMsgRewritesVersionAndSender(t *testing.T) { + dst := HashID("dst") + sender := HashID("sender") + payload := []byte("payload") + msg, err := MarshalTransportMsg(dst, payload) + require.NoError(t, err) + + require.NoError(t, UpdateTransportMsg(msg, ProtocolVersion(2), sender)) + assert.Equal(t, byte(2), msg[0], "the header must carry the destination version") + assert.Equal(t, byte(MsgTypeTransport), msg[1], "the message type must be preserved") + + gotID, gotPayload, err := UnmarshalTransportMsg(msg) + require.NoError(t, err) + assert.Equal(t, sender, *gotID, "the peer ID must be rewritten to the sender") + assert.Equal(t, payload, gotPayload, "the payload must be preserved") + + assert.ErrorIs(t, UpdateTransportMsg(msg[:headerTotalSizeTransport-1], ProtocolV1, sender), ErrInvalidMessageLength) +} diff --git a/shared/relay/messages/peer_state.go b/shared/relay/messages/peer_state.go index f10bc7bdf..42b1762b5 100644 --- a/shared/relay/messages/peer_state.go +++ b/shared/relay/messages/peer_state.go @@ -5,7 +5,7 @@ import ( ) func MarshalSubPeerStateMsg(ids []PeerID) ([][]byte, error) { - return marshalPeerIDs(ids, byte(MsgTypeSubscribePeerState)) + return marshalPeerIDs(CurrentProtocolVersion, ids, byte(MsgTypeSubscribePeerState)) } func UnmarshalSubPeerStateMsg(buf []byte) ([]PeerID, error) { @@ -13,7 +13,7 @@ func UnmarshalSubPeerStateMsg(buf []byte) ([]PeerID, error) { } func MarshalUnsubPeerStateMsg(ids []PeerID) ([][]byte, error) { - return marshalPeerIDs(ids, byte(MsgTypeUnsubscribePeerState)) + return marshalPeerIDs(CurrentProtocolVersion, ids, byte(MsgTypeUnsubscribePeerState)) } func UnmarshalUnsubPeerStateMsg(buf []byte) ([]PeerID, error) { @@ -21,7 +21,7 @@ func UnmarshalUnsubPeerStateMsg(buf []byte) ([]PeerID, error) { } func MarshalPeersOnline(ids []PeerID) ([][]byte, error) { - return marshalPeerIDs(ids, byte(MsgTypePeersOnline)) + return marshalPeerIDs(CurrentProtocolVersion, ids, byte(MsgTypePeersOnline)) } func UnmarshalPeersOnlineMsg(buf []byte) ([]PeerID, error) { @@ -29,7 +29,7 @@ func UnmarshalPeersOnlineMsg(buf []byte) ([]PeerID, error) { } func MarshalPeersWentOffline(ids []PeerID) ([][]byte, error) { - return marshalPeerIDs(ids, byte(MsgTypePeersWentOffline)) + return marshalPeerIDs(CurrentProtocolVersion, ids, byte(MsgTypePeersWentOffline)) } func UnMarshalPeersWentOffline(buf []byte) ([]PeerID, error) { @@ -37,7 +37,7 @@ func UnMarshalPeersWentOffline(buf []byte) ([]PeerID, error) { } // marshalPeerIDs is a generic function to marshal peer IDs with a specific message type -func marshalPeerIDs(ids []PeerID, msgType byte) ([][]byte, error) { +func marshalPeerIDs(version ProtocolVersion, ids []PeerID, msgType byte) ([][]byte, error) { if len(ids) == 0 { return nil, fmt.Errorf("no list of peer ids provided") } @@ -54,7 +54,7 @@ func marshalPeerIDs(ids []PeerID, msgType byte) ([][]byte, error) { totalSize := sizeOfProtoHeader + len(chunk)*peerIDSize buf := make([]byte, totalSize) - buf[0] = byte(CurrentProtocolVersion) + buf[0] = byte(version) buf[1] = msgType offset := sizeOfProtoHeader