From 1e63b68c4dc243a32fef823df1bc024c997a9c60 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 21 Oct 2026 12:27:44 +0200 Subject: [PATCH 01/14] [relay] Negotiate the protocol version on the server side (#8231) --- relay/metrics/realy.go | 54 ++-- relay/server/handshake.go | 59 ++-- relay/server/handshake_test.go | 378 ++++++++++++++++++++++++++ relay/server/peer.go | 34 ++- relay/server/peer_test.go | 99 +++++++ relay/server/relay.go | 20 +- relay/server/ws_version_test.go | 97 +++++++ shared/relay/messages/message.go | 220 +++++++++++++-- shared/relay/messages/message_test.go | 246 +++++++++++++++++ shared/relay/messages/peer_state.go | 12 +- 10 files changed, 1128 insertions(+), 91 deletions(-) create mode 100644 relay/server/handshake_test.go create mode 100644 relay/server/peer_test.go create mode 100644 relay/server/ws_version_test.go 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/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 From 031a4bd70de40ec286197a7c6de58613a0692293 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Wed, 21 Oct 2026 15:53:05 +0200 Subject: [PATCH 02/14] [client] Migrate the desktop UI to Tailwind CSS 4 and fix frontend npm audit findings (#8277) * [proxy] Fix npm audit findings in the web UI npm audit reported 10 vulnerabilities (7 high), all in build tooling: Vite/launch-editor, PostCSS, Babel, browserslist, js-yaml, nanoid, brace-expansion and source-map-js. All are resolved by the lockfile update; Vite is pinned exactly, so it is bumped from 7.3.2 to 7.3.7. * [client] Migrate the desktop UI to Tailwind CSS 4 and fix npm audit findings pnpm audit reported 36 vulnerabilities in the desktop UI. All but two (braces and postcss-selector-parser, both pulled in by Tailwind 3 with no patched version in range) are fixed by updating within the existing ranges; react-router moves to 7.18.4, which also fixes an open redirect and an XSS in its error handler. The remaining two go away by moving to Tailwind CSS 4, which the proxy web UI already uses: - The config moves into an @theme block in globals.css; the official upgrade tool applied the v4 utility renames across the components. - @tailwindcss/vite replaces the PostCSS plugin and autoprefixer. - tw-animate-css replaces tailwindcss-animate with the same classes. - The Prettier plugin reads the theme from the stylesheet. A screenshot comparison of every screen against the Tailwind 3 build found three regressions caused by v4's changed utility and variant ordering, fixed here so the UI renders as before: - Label combined inline-block and flex; v4 let inline-block win, which made every settings row taller. The dead inline-block is removed. - The active settings tab lost its highlight in dark mode because dark: now outranks group-data-[state=active]:. The active color is set for dark mode explicitly. - v4 rebuilt its default palette in OKLCH, shifting colors the theme does not override such as sky-400. The Tailwind 3 values for the neutral, zinc, sky, orange and amber families are pinned in @theme. The only remaining visible difference is 2px less height per row in the profiles list, because v4's preflight resets the browser's default table-cell padding. --- client/ui/frontend/.prettierrc | 1 + client/ui/frontend/package.json | 8 +- client/ui/frontend/pnpm-lock.yaml | 2755 ++++++++--------- client/ui/frontend/postcss.config.js | 6 - client/ui/frontend/src/components/Badge.tsx | 2 +- .../src/components/CopyToClipboard.tsx | 12 +- .../frontend/src/components/DropdownMenu.tsx | 28 +- .../src/components/LanguagePicker.tsx | 12 +- .../src/components/ManagementServerSwitch.tsx | 2 +- client/ui/frontend/src/components/Tooltip.tsx | 4 +- .../frontend/src/components/VerticalTabs.tsx | 10 +- .../src/components/buttons/Button.tsx | 22 +- .../src/components/buttons/IconButton.tsx | 2 +- .../src/components/dialog/ConfirmDialog.tsx | 4 +- .../frontend/src/components/dialog/Dialog.tsx | 12 +- .../components/dialog/DialogDescription.tsx | 2 +- .../src/components/dialog/DialogHeading.tsx | 2 +- .../empty-state/DaemonOutdatedOverlay.tsx | 2 +- .../empty-state/DaemonUnavailableOverlay.tsx | 2 +- .../src/components/empty-state/NoResults.tsx | 2 +- .../empty-state/NotConnectedState.tsx | 2 +- .../frontend/src/components/inputs/Input.tsx | 22 +- .../src/components/inputs/SearchInput.tsx | 4 +- .../frontend/src/components/inputs/Select.tsx | 4 +- .../components/switches/FancyToggleSwitch.tsx | 4 +- .../src/components/switches/SwitchItem.tsx | 4 +- .../src/components/switches/ToggleSwitch.tsx | 8 +- .../src/components/typography/Label.tsx | 4 +- client/ui/frontend/src/globals.css | 286 +- .../src/modules/error/ErrorDialog.tsx | 4 +- .../main/MainConnectionStatusSwitch.tsx | 18 +- .../src/modules/main/MainExitNodeSwitcher.tsx | 20 +- .../frontend/src/modules/main/MainHeader.tsx | 6 +- .../src/modules/main/advanced/Navigation.tsx | 4 +- .../main/advanced/networks/NetworkFilters.tsx | 4 +- .../main/advanced/networks/Networks.tsx | 20 +- .../main/advanced/peers/PeerDetailPanel.tsx | 30 +- .../main/advanced/peers/PeerFilters.tsx | 4 +- .../src/modules/main/advanced/peers/Peers.tsx | 8 +- .../src/modules/profiles/ProfileAvatar.tsx | 2 +- .../src/modules/profiles/ProfileDropdown.tsx | 24 +- .../src/modules/profiles/ProfilesTab.tsx | 18 +- .../session/SessionExpirationDialog.tsx | 2 +- .../src/modules/settings/SettingsAbout.tsx | 8 +- .../modules/settings/SettingsNavigation.tsx | 2 +- .../src/modules/settings/SettingsPage.tsx | 2 +- .../src/modules/settings/SettingsSSH.tsx | 2 +- .../src/modules/settings/SettingsSection.tsx | 6 +- .../settings/SettingsTroubleshooting.tsx | 4 +- .../src/modules/welcome/WelcomeStepTray.tsx | 2 +- client/ui/frontend/tailwind.config.ts | 180 -- client/ui/frontend/vite.config.ts | 3 +- proxy/web/package-lock.json | 319 +- proxy/web/package.json | 2 +- 54 files changed, 1913 insertions(+), 2009 deletions(-) delete mode 100644 client/ui/frontend/postcss.config.js delete mode 100644 client/ui/frontend/tailwind.config.ts 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<
@@ -164,7 +164,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", )} />
@@ -195,7 +195,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", )} @@ -224,7 +224,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 86589e47f..74f99752c 100644 --- a/client/ui/frontend/src/components/Tooltip.tsx +++ b/client/ui/frontend/src/components/Tooltip.tsx @@ -84,8 +84,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 89c121e21..4b1363c16 100644 --- a/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx +++ b/client/ui/frontend/src/components/empty-state/DaemonUnavailableOverlay.tsx @@ -19,7 +19,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 df48524c1..e0fc5baf6 100644 --- a/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx +++ b/client/ui/frontend/src/modules/main/MainConnectionStatusSwitch.tsx @@ -255,7 +255,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])} @@ -267,7 +267,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", )} > @@ -299,7 +299,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", )} > @@ -327,7 +327,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", @@ -346,7 +346,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", @@ -361,9 +361,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", )} > @@ -401,9 +401,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 ec70fb923..34d87d928 100644 --- a/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfileDropdown.tsx @@ -94,8 +94,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", @@ -108,12 +108,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 && ( <> @@ -135,7 +139,7 @@ export const ProfileDropdown = ({ onManageProfiles }: ProfileDropdownProps) => { @@ -157,7 +161,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", )} @@ -185,13 +189,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"} >
    -
    +
    ); @@ -213,7 +217,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 509f48061..ae43c6d06 100644 --- a/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx +++ b/client/ui/frontend/src/modules/profiles/ProfilesTab.tsx @@ -204,7 +204,7 @@ export function ProfilesTab() {

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

    -

    +

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

    @@ -409,10 +409,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} @@ -470,7 +470,7 @@ const TruncatedEmail = ({ email }: { email: string }) => { const span = ( {email} @@ -558,7 +558,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", @@ -605,7 +605,7 @@ const DeleteMenuItem = ({ disabled, disabledReason, onDelete }: DeleteMenuItemPr
    @@ -653,7 +653,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" @@ -661,7 +661,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 e57040a7a..9ca324781 100644 --- a/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx +++ b/client/ui/frontend/src/modules/session/SessionExpirationDialog.tsx @@ -221,7 +221,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 7228e2a6c..23fa4168a 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/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" } } From 11799f6b94713025c79ccdd299d6aa901fc2f1aa Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 21 Oct 2026 18:33:31 +0300 Subject: [PATCH 03/14] [management] Use account owner as inviter for service user invites (#8276) --- management/server/user.go | 4 +-- management/server/user_test.go | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/management/server/user.go b/management/server/user.go index e64320666..84c4c5022 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 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 From 4ca20f0c7a6f806c7b9ac27f1aa8e14ec4e8cd52 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 21 Oct 2026 18:38:08 +0300 Subject: [PATCH 04/14] [management] Make EDR sync interval configurable (#8273) --- shared/management/http/api/openapi.yml | 45 +++++++++++++++++++++++++ shared/management/http/api/types.gen.go | 30 +++++++++++++++++ 2 files changed, 75 insertions(+) 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"` } From 73829c8d968bc2c0a8dd423449638ecc57134adf Mon Sep 17 00:00:00 2001 From: Mohammadreza Razavian <62871170+smrrazavian@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:16:33 +0330 Subject: [PATCH 05/14] [management] Give the /api/setup owner account the single account mode domain (#8147) --- management/server/account_test.go | 44 ++++++++++ .../instance/instance_handler_test.go | 18 ++--- management/server/instance/setup_service.go | 29 ++++++- .../server/instance/setup_service_test.go | 81 +++++++++++++++---- 4 files changed, 145 insertions(+), 27 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 8f7b1f18b..983ee8680 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -50,6 +50,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 +845,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) 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/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") From b03d1c7d0794dec3d96072c98e01811535ace19a Mon Sep 17 00:00:00 2001 From: Bruno Mercier Costa <94469565+emrcbrn@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:37:48 +0200 Subject: [PATCH 06/14] [client] Stop the service and UI before the NSIS installer extracts files (#8255) --- client/installer.nsis | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From f886f4b06ce00f7ab14afd7ce53e41afd4bfe108 Mon Sep 17 00:00:00 2001 From: Theodor Midtlien Date: Thu, 22 Oct 2026 09:47:39 +0200 Subject: [PATCH 07/14] Bump wasm build size to 70MB (#8281) --- .github/workflows/wasm-build-validation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From abf50e3ad4bde92c0cb8136e536bd8e622011cc1 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:39:22 +0900 Subject: [PATCH 08/14] [client] Remove the replace directives from the embedded client (#8280) --- client/embed/embed.go | 9 +++-- client/firewall/iface.go | 5 ++- client/firewall/uspfilter/filter.go | 4 +-- .../firewall/uspfilter/filter_filter_test.go | 10 +++--- .../uspfilter/filter_routeacl_test.go | 28 +++++++-------- client/firewall/uspfilter/filter_test.go | 6 ++-- .../firewall/uspfilter/forwarder/endpoint.go | 4 +-- .../firewall/uspfilter/forwarder/forwarder.go | 4 +-- client/iface/bind/control.go | 5 ++- client/iface/bind/endpoint.go | 4 +-- client/iface/bind/ice_bind.go | 30 ++++++++-------- client/iface/bind/ice_bind_close_test.go | 11 +++--- client/iface/bind/ice_bind_test.go | 9 +++-- client/iface/bind/relay_bind.go | 14 ++++---- client/iface/bind/stun_filter_test.go | 5 +-- client/iface/configurer/uapi.go | 6 ++-- client/iface/configurer/uapi_windows.go | 4 +-- client/iface/configurer/usp.go | 35 +++++++++---------- .../iface/configurer/usp_allowedips_test.go | 5 ++- client/iface/configurer/usp_test.go | 14 ++++---- client/iface/device.go | 9 ++--- client/iface/device/device_android.go | 16 ++++----- client/iface/device/device_darwin.go | 16 ++++----- client/iface/device/device_filter.go | 15 ++++---- client/iface/device/device_ios.go | 16 ++++----- client/iface/device/device_kernel_unix.go | 7 ++-- client/iface/device/device_netstack.go | 18 +++++----- client/iface/device/device_usp_unix.go | 16 ++++----- client/iface/device/device_windows.go | 20 +++++------ client/iface/device/renewable_tun.go | 25 ++++++------- client/iface/device/wg_log.go | 6 ++-- client/iface/device_android.go | 9 ++--- client/iface/iface.go | 8 ++--- client/iface/iface_close_test.go | 8 ++--- client/iface/mocks/tun.go | 4 +-- client/iface/netstack/dialer.go | 7 ++-- client/iface/netstack/tun.go | 10 +++--- client/iface/udpmux/mux.go | 2 +- client/iface/wgbackend/conn.go | 27 ++++++++++++++ client/iface/wgbackend/device.go | 24 +++++++++++++ client/iface/wgbackend/doc.go | 5 +++ client/iface/wgbackend/ipc_unix.go | 10 ++++++ client/iface/wgbackend/ipc_windows.go | 5 +++ client/iface/wgbackend/tun.go | 19 ++++++++++ client/iface/wgbackend/tun_linux.go | 5 +++ client/iface/wgbackend/tun_unix.go | 10 ++++++ client/iface/wgbackend/tun_windows.go | 7 ++++ client/internal/acl/mocks/iface_mapper.go | 6 ++-- client/internal/dns/response_writer.go | 5 +-- client/internal/dns/server_test.go | 4 +-- client/internal/dns/service_memory.go | 4 +-- client/internal/dns/tcpstack.go | 9 ++--- client/internal/dns/upstream.go | 6 ++-- client/internal/dns/upstream_general.go | 4 +-- client/internal/dns/upstream_test.go | 4 +-- client/internal/dns/wgiface.go | 5 ++- client/internal/dns/wgiface_windows.go | 5 ++- client/internal/dnsfwd/forwarder.go | 8 ++--- client/internal/dnsfwd/manager.go | 4 +-- client/internal/engine.go | 9 +++-- client/internal/engine_test.go | 11 +++--- client/internal/iface_common.go | 7 ++-- client/internal/peer/conn.go | 2 +- client/internal/peer/guard/ice_monitor.go | 2 +- client/internal/peer/ice/agent.go | 2 +- client/internal/peer/ice/config.go | 2 +- client/internal/peer/signaler.go | 2 +- client/internal/peer/worker_ice.go | 2 +- client/internal/peer/worker_ice_test.go | 2 +- .../routemanager/iface/iface_common.go | 5 ++- .../internal/routemanager/reconcile_test.go | 4 +-- client/ssh/server/server.go | 6 ++-- combined/cmd/admin.go | 2 +- go.mod | 26 +++++--------- go.sum | 27 +++++++------- idp/dex/config.go | 6 ++-- idp/dex/connector.go | 2 +- idp/dex/connector_test.go | 4 +-- idp/dex/provider.go | 8 ++--- idp/dex/provider_test.go | 6 ++-- idp/dex/sqlite_cgo.go | 2 +- idp/dex/sqlite_nocgo.go | 2 +- idp/sdk/sdk.go | 2 +- management/cmd/admin.go | 2 +- management/cmd/admin/admin.go | 2 +- management/cmd/admin/admin_test.go | 4 +-- management/server/identity_provider.go | 2 +- management/server/idp/dex.go | 2 +- management/server/idp/embedded.go | 2 +- management/server/instance/manager.go | 2 +- management/server/instance/manager_test.go | 2 +- 91 files changed, 418 insertions(+), 343 deletions(-) create mode 100644 client/iface/wgbackend/conn.go create mode 100644 client/iface/wgbackend/device.go create mode 100644 client/iface/wgbackend/doc.go create mode 100644 client/iface/wgbackend/ipc_unix.go create mode 100644 client/iface/wgbackend/ipc_windows.go create mode 100644 client/iface/wgbackend/tun.go create mode 100644 client/iface/wgbackend/tun_linux.go create mode 100644 client/iface/wgbackend/tun_unix.go create mode 100644 client/iface/wgbackend/tun_windows.go 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/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/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/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" From 0fc596e2e1a80aacf2502ce7b750196b6b510fcd Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 22 Oct 2026 14:32:52 +0200 Subject: [PATCH 09/14] [management] Skip inactivity expiration rescan on connect when a job is scheduled (#8287) * [management] Skip inactivity expiration rescan when a job is scheduled Every SSO peer connect and disconnect cancelled the account's inactivity expiration job and re-armed it, loading every inactivity-enabled peer of the account each time. During a management deploy, when all peers reconnect, that is a full account scan per peer. A connecting peer cannot expire, and a disconnecting peer expires after every peer that was already disconnected, so neither can move the next run earlier. Connect and disconnect now leave an already scheduled job in place, as login expiration already does. Settings and per-peer toggle changes still cancel and reschedule, since they can move the run earlier. * [management] Keep inactivity rescan on disconnect A disconnect that lands while the inactivity job is finishing its last run saw the job as scheduled and skipped arming a new one; the scheduler then removed the job, leaving the disconnected peer without an inactivity timer. Only connect, where the peer cannot expire, now skips a scheduled job; disconnect cancels and re-arms as before. The test asserts on the synchronous Cancel call instead of sleeping. --- management/server/account.go | 15 +++++++++ management/server/account_test.go | 52 +++++++++++++++++++++++++++++++ management/server/peer.go | 4 +-- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 68150a947..7cab037a2 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -806,6 +806,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) { diff --git a/management/server/account_test.go b/management/server/account_test.go index 983ee8680..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" @@ -2277,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/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 } From c476196653658dc872b93458e9385a606f778a70 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:12:17 +0200 Subject: [PATCH 10/14] [management] Add additional indexes for groups, events, pats and access logs (#8289) --- .../modules/reverseproxy/accesslogs/accesslogentry.go | 4 ++-- management/server/activity/event.go | 4 ++-- management/server/types/group.go | 2 +- management/server/types/personal_access_token.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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/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/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..8351928b5 100644 --- a/management/server/types/personal_access_token.go +++ b/management/server/types/personal_access_token.go @@ -31,7 +31,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 From 431e20135521f5f7a8bb425f9c9ea86bf0df9f93 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:13:11 +0200 Subject: [PATCH 11/14] [management] reduce calls to update last usage for users and pats (#8290) --- .../server/http/middleware/auth_middleware.go | 7 ++- .../http/middleware/auth_middleware_test.go | 63 +++++++++++++++++++ .../store/sql_store_personal_access_token.go | 26 +++++--- .../sql_store_personal_access_token_test.go | 19 ++++++ management/server/store/sql_store_user.go | 34 ++++++---- .../server/store/sql_store_user_test.go | 47 ++++++++++++++ .../server/types/personal_access_token.go | 2 + management/server/user.go | 7 ++- 8 files changed, 178 insertions(+), 27 deletions(-) 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/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/personal_access_token.go b/management/server/types/personal_access_token.go index 8351928b5..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 diff --git a/management/server/user.go b/management/server/user.go index 84c4c5022..3e84fcd03 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -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 { From 48ecdfbcfc7f81154c4eab4976079089eac622ae Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:13:55 +0200 Subject: [PATCH 12/14] [management] remove account id check from inner groups query to use index (#8297) --- management/internals/network_map_db/pgsql/group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ` From f548a74c7c5b7cde2d5cdc658737b1eedff3e90c Mon Sep 17 00:00:00 2001 From: dmitri-netbird Date: Thu, 22 Oct 2026 20:27:53 +0200 Subject: [PATCH 13/14] [management] Compensate for a possible nil peer when returning "empty" network map (#8285) * we somehow can get a nil peer when returning empty components; this fix compensates for the issue, but doesn't fix the root cause (which is unknown atm) Signed-off-by: Dmitri Dolguikh * fix test Signed-off-by: Dmitri Dolguikh --------- Signed-off-by: Dmitri Dolguikh --- .../controllers/network_map/controller/controller.go | 2 +- management/internals/shared/grpc/components_encoder.go | 5 +++-- management/internals/shared/grpc/components_encoder_test.go | 3 +++ .../internals/shared/grpc/components_envelope_response.go | 1 + shared/management/networkmap/envelope_test.go | 1 + 5 files changed, 9 insertions(+), 3 deletions(-) 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/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/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", }) From 8397bed4afff8b2e554f8d6ccc5d5f6684972967 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 22 Oct 2026 22:57:47 +0200 Subject: [PATCH 14/14] [management] Push network maps when background validation changes (#8299) An integrated validator can approve or revoke peers outside a login or sync, for example during a periodic EDR sync. Only revocations of SSO peers reached anyone; a newly approved peer kept its empty map, and its peers did not see it, until an unrelated update or a reconnect. Add SetPeerValidationChangeListener to the IntegratedValidator interface. The account manager registers a handler that resolves the affected peers and calls OnPeersUpdated, the same path a login-time approval takes. The OSS validator and the mock implement it as a no-op. --- management/server/account.go | 19 ++++++ management/server/integrated_validator.go | 4 ++ .../integrated_validator_mock.go | 12 ++++ .../integrated_validator/interface.go | 3 + .../validator/validator.go | 3 + .../server/peer_validation_change_test.go | 59 +++++++++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 management/server/peer_validation_change_test.go diff --git a/management/server/account.go b/management/server/account.go index 7cab037a2..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. @@ -2102,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/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_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) +}