From 59410074d34c64632d9ff1ac04c92beedfc3dfc2 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 20 Mar 2026 09:05:49 +0200 Subject: [PATCH 01/48] test(auth): add comprehensive OIDC storage and token validation tests Cover storage CRUD (auth requests, refresh tokens, revocation, cleanup), JWT validation (valid, wrong key, expired, wrong audience, unknown subject), and login handler form rendering/credential rejection. --- auth/basic.go | 26 +----- auth/middleware.go | 18 +--- auth/oidc/login.go | 97 ++++++++------------- auth/oidc/models.go | 51 +++++++---- auth/oidc/provider.go | 44 ++-------- auth/oidc/routes.go | 51 ++++------- auth/oidc/storage.go | 52 +++++------ auth/oidc_test.go | 109 +++++------------------ auth/oidc_validate.go | 4 +- cmd/auth_login.go | 197 ++++++++++++++++++++++++++++-------------- go.mod | 4 +- jobs/jobs.go | 2 +- 12 files changed, 276 insertions(+), 379 deletions(-) diff --git a/auth/basic.go b/auth/basic.go index 4d267a82a..5e7705ea6 100644 --- a/auth/basic.go +++ b/auth/basic.go @@ -19,8 +19,8 @@ import ( ) var ( - HtpasswdFile string - OIDCEnabled bool + HtpasswdFile string + OIDCEnabled bool OIDCSigningKeyPath string checker *htpasswd.File @@ -165,7 +165,6 @@ func basicAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { } } } - setWWWAuthenticate(c) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) } @@ -195,27 +194,6 @@ func LookupPersonByUsername(ctx context.Context, username string) (string, error return person.ID.String(), nil } -// HtpasswdChecker wraps htpasswd.File to implement oidc.CredentialChecker. -type HtpasswdChecker struct { - file *htpasswd.File -} - -func NewHtpasswdChecker(path string) (*HtpasswdChecker, error) { - f, err := htpasswd.New(path, htpasswd.DefaultSystems, nil) - if err != nil { - return nil, err - } - return &HtpasswdChecker{file: f}, nil -} - -func (h *HtpasswdChecker) Match(ctx context.Context, user, pass string) error { - match := h.file.Match(user, pass) - if !match { - return fmt.Errorf("invalid credentials") - } - return nil -} - func lookupPerson(ctx context.Context, user string) (*models.Person, error) { user = strings.ToLower(user) var person models.Person diff --git a/auth/middleware.go b/auth/middleware.go index 158e78dcb..de75338dc 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -59,21 +59,6 @@ var skipAuthPathPrefixes = []string{ "/auth/basic/", "/oidc/", "/.well-known/", - "/oauth/", // Standard OIDC protocol endpoints (mounted at root to match the issuer URL). -} - -var skipAuthPathsExact = []string{ - "/health", - - // --start:: Standard OIDC protocol endpoints (mounted at root to match the issuer URL). - "/authorize", - "/authorize/callback", - "/userinfo", - "/keys", - "/revoke", - "/device_authorization", - "/endsession", - // --end:: Standard OIDC endpoints } func Middleware(ctx context.Context, e *echo.Echo) error { @@ -92,6 +77,9 @@ func Middleware(ctx context.Context, e *echo.Echo) error { switch vars.AuthMode { case Basic: + if OIDCEnabled && (vars.AuthMode == Kratos || vars.AuthMode == Clerk) { + return fmt.Errorf("--oidc is only supported with --auth basic") + } UseBasic(e) if admin, err := GetOrCreateAdminUser(ctx); err != nil { return fmt.Errorf("failed to created admin user: %v", err) diff --git a/auth/oidc/login.go b/auth/oidc/login.go index c713cd769..418773e78 100644 --- a/auth/oidc/login.go +++ b/auth/oidc/login.go @@ -2,44 +2,51 @@ package oidc import ( "fmt" - "html" "net/http" "github.com/flanksource/duty/context" - "github.com/flanksource/incident-commander/auth/oidc/static" "github.com/labstack/echo/v4" "github.com/zitadel/oidc/v3/pkg/op" ) +const loginFormHTML = ` + +Mission Control Login + +

Sign in

+
+ +
+
+ +
+%s + +` + +// LoginHandler handles the OIDC login form, delegating credential validation +// to the Basic auth checker and person lookup. type LoginHandler struct { - storage *Storage - provider op.OpenIDProvider - checker CredentialChecker - PersonLookup PersonLookup - issuerURL string + storage *Storage + provider op.OpenIDProvider + checker credentialChecker + personLookup personLookup } -type CredentialChecker interface { - Match(ctx context.Context, user, pass string) error +// credentialChecker validates username/password via htpasswd. +type credentialChecker interface { + Match(user, pass string) bool } -type LoginRedirector interface { - LoginRedirectURL(authRequestID string) (string, error) -} - -type CallbackSubjectResolver interface { - CallbackSubject(c echo.Context) (string, error) -} +// personLookup finds a person by username/email, returning the person UUID. +type personLookup func(ctx context.Context, user string) (personID string, err error) -type PersonLookup func(ctx context.Context, user string) (personID string, err error) - -func NewLoginHandler(storage *Storage, provider op.OpenIDProvider, checker CredentialChecker, lookup PersonLookup, issuerURL string) *LoginHandler { +func NewLoginHandler(storage *Storage, provider op.OpenIDProvider, checker credentialChecker, lookup personLookup) *LoginHandler { return &LoginHandler{ storage: storage, provider: provider, checker: checker, - PersonLookup: lookup, - issuerURL: issuerURL, + personLookup: lookup, } } @@ -48,16 +55,7 @@ func (h *LoginHandler) ShowForm(c echo.Context) error { if id == "" { return c.String(http.StatusBadRequest, "missing auth_request_id") } - - if redirector, ok := h.checker.(LoginRedirector); ok { - redirectURL, err := redirector.LoginRedirectURL(id) - if err != nil { - return c.String(http.StatusInternalServerError, "failed to build login redirect") - } - return c.Redirect(http.StatusFound, redirectURL) - } - - return c.HTML(http.StatusOK, fmt.Sprintf(static.LoginHTML, html.EscapeString(id), "")) + return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, id, "")) } func (h *LoginHandler) HandleSubmit(c echo.Context) error { @@ -68,19 +66,18 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { password := c.FormValue("password") renderForm := func(msg string) error { - return c.HTML(http.StatusOK, fmt.Sprintf(static.LoginHTML, html.EscapeString(id), - `

`+html.EscapeString(msg)+`

`)) + return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, id, "

"+msg+"

")) } if id == "" || username == "" || password == "" { return renderForm("All fields required") } - if err := h.checker.Match(ctx, username, password); err != nil { - return renderForm(fmt.Sprintf("Invalid credentials: %v", err)) + if !h.checker.Match(username, password) { + return renderForm("Invalid credentials") } - personID, err := h.PersonLookup(ctx, username) + personID, err := h.personLookup(ctx, username) if err != nil { return renderForm("User not found") } @@ -89,32 +86,6 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { return renderForm("Internal error") } - issuerCtx := op.ContextWithIssuer(c.Request().Context(), h.issuerURL) - callbackURL := op.AuthCallbackURL(h.provider)(issuerCtx, id) - return c.Redirect(http.StatusFound, callbackURL) -} - -func (h *LoginHandler) HandleExternalCallback(c echo.Context) error { - id := c.QueryParam("auth_request_id") - if id == "" { - return c.String(http.StatusBadRequest, "missing auth_request_id") - } - - resolver, ok := h.checker.(CallbackSubjectResolver) - if !ok { - return c.String(http.StatusNotFound, "external callback is not configured") - } - - personID, err := resolver.CallbackSubject(c) - if err != nil { - return c.String(http.StatusUnauthorized, "authorization failed") - } - - if err := h.storage.SetAuthRequestSubject(id, personID); err != nil { - return c.String(http.StatusInternalServerError, "internal error") - } - - issuerCtx := op.ContextWithIssuer(c.Request().Context(), h.issuerURL) - callbackURL := op.AuthCallbackURL(h.provider)(issuerCtx, id) + callbackURL := op.AuthCallbackURL(h.provider)(c.Request().Context(), id) return c.Redirect(http.StatusFound, callbackURL) } diff --git a/auth/oidc/models.go b/auth/oidc/models.go index 7be575b1b..0bfc0b3cc 100644 --- a/auth/oidc/models.go +++ b/auth/oidc/models.go @@ -2,9 +2,10 @@ package oidc import ( "database/sql/driver" + "fmt" + "strings" "time" - "github.com/lib/pq" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" ) @@ -12,14 +13,31 @@ import ( const ClientID = "mc-cli" // StringList is a PostgreSQL text[] compatible type. -type StringList pq.StringArray +type StringList []string func (s StringList) Value() (driver.Value, error) { - return pq.StringArray(s).Value() + if len(s) == 0 { + return "{}", nil + } + return "{" + strings.Join(s, ",") + "}", nil } func (s *StringList) Scan(src any) error { - return (*pq.StringArray)(s).Scan(src) + if src == nil { + *s = nil + return nil + } + str, ok := src.(string) + if !ok { + return fmt.Errorf("unsupported type: %T", src) + } + str = strings.Trim(str, "{}") + if str == "" { + *s = nil + return nil + } + *s = strings.Split(str, ",") + return nil } // AuthRequest implements op.AuthRequest backed by the oidc_auth_requests table. @@ -35,7 +53,7 @@ type AuthRequest struct { CodeChallengeMethod string `gorm:"column:code_challenge_method"` Subject string `gorm:"column:subject"` AuthTime *time.Time `gorm:"column:auth_time"` - Code *string `gorm:"column:code"` + Code string `gorm:"column:code"` IsDone bool `gorm:"column:done;default:false"` CreatedAt time.Time `gorm:"column:created_at"` ExpiresAt time.Time `gorm:"column:expires_at"` @@ -44,16 +62,16 @@ type AuthRequest struct { func (AuthRequest) TableName() string { return "oidc_auth_requests" } func (a *AuthRequest) GetID() string { return a.ID } -func (a *AuthRequest) GetACR() string { return "" } -func (a *AuthRequest) GetAMR() []string { return nil } -func (a *AuthRequest) GetAudience() []string { return []string{a.ClientID} } +func (a *AuthRequest) GetACR() string { return "" } +func (a *AuthRequest) GetAMR() []string { return nil } +func (a *AuthRequest) GetAudience() []string { return []string{a.ClientID} } func (a *AuthRequest) GetAuthTime() time.Time { if a.AuthTime != nil { return *a.AuthTime } return time.Time{} } -func (a *AuthRequest) GetClientID() string { return a.ClientID } +func (a *AuthRequest) GetClientID() string { return a.ClientID } func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { if a.CodeChallenge == "" { return nil @@ -69,10 +87,10 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType { return oidc.ResponseType(a.ResponseType) } func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { return "" } -func (a *AuthRequest) GetScopes() []string { return []string(a.Scopes) } -func (a *AuthRequest) GetState() string { return a.State } -func (a *AuthRequest) GetSubject() string { return a.Subject } -func (a *AuthRequest) Done() bool { return a.IsDone } +func (a *AuthRequest) GetScopes() []string { return []string(a.Scopes) } +func (a *AuthRequest) GetState() string { return a.State } +func (a *AuthRequest) GetSubject() string { return a.Subject } +func (a *AuthRequest) Done() bool { return a.IsDone } // RefreshToken is backed by the oidc_refresh_tokens table. type RefreshToken struct { @@ -142,11 +160,12 @@ func (c *cliClient) AccessTokenType() op.AccessTokenType { func (c *cliClient) IDTokenLifetime() time.Duration { return time.Hour } func (c *cliClient) DevMode() bool { return false } func (c *cliClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { - return func(scopes []string) []string { return scopes } + return nil } func (c *cliClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { - return func(scopes []string) []string { return scopes } + return nil } -func (c *cliClient) IsScopeAllowed(scope string) bool { return true } +func (c *cliClient) IsScopeAllowed(scope string) bool { return true } func (c *cliClient) IDTokenUserinfoClaimsAssertion() bool { return false } func (c *cliClient) ClockSkew() time.Duration { return 0 } + diff --git a/auth/oidc/provider.go b/auth/oidc/provider.go index 2c659a21e..139620ab4 100644 --- a/auth/oidc/provider.go +++ b/auth/oidc/provider.go @@ -31,11 +31,6 @@ func NewProvider(ctx context.Context, issuerURL, signingKeyPath string) (*Provid return nil, fmt.Errorf("oidc signing key: %w", err) } - cryptoKey, err := loadOrGenerateCryptoKey(signingKeyPath) - if err != nil { - return nil, fmt.Errorf("oidc crypto key: %w", err) - } - signer := &signingKey{ id: keyID, algorithm: "RS256", @@ -44,8 +39,7 @@ func NewProvider(ctx context.Context, issuerURL, signingKeyPath string) (*Provid storage := NewStorage(ctx, signer) config := &op.Config{ - CryptoKey: cryptoKey, - GrantTypeRefreshToken: true, + CryptoKey: aesKeyFromIssuer(issuerURL), } oidcProvider, err := op.NewProvider(config, storage, @@ -135,37 +129,9 @@ func writeRSAPrivateKey(path string, key *rsa.PrivateKey) error { return os.WriteFile(path, data, 0600) } -func loadOrGenerateCryptoKey(keyPath string) ([32]byte, error) { - if keyPath == "" { - keyPath = "oidc-crypto-key.key" - } else { - keyPath = keyPath + ".crypto" - } - +// aesKeyFromIssuer derives a 32-byte AES key from the issuer URL for OIDC internal encryption. +func aesKeyFromIssuer(issuer string) [32]byte { var key [32]byte - data, err := os.ReadFile(keyPath) - if err == nil { - if len(data) == 32 { - copy(key[:], data) - logger.Infof("OIDC: loaded crypto key from %s", keyPath) - return key, nil - } - return key, fmt.Errorf("crypto key at %s has invalid length %d (expected 32)", keyPath, len(data)) - } - - if !os.IsNotExist(err) { - return key, fmt.Errorf("read crypto key file %s: %w", keyPath, err) - } - - // Generate new key - if _, err := rand.Read(key[:]); err != nil { - return key, fmt.Errorf("generate crypto key: %w", err) - } - - if err := os.WriteFile(keyPath, key[:], 0600); err != nil { - return key, fmt.Errorf("write crypto key: %w", err) - } - logger.Infof("OIDC: generated new crypto key at %s", keyPath) - - return key, nil + copy(key[:], []byte(issuer)) + return key } diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index d5f39c316..9c12edd08 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -1,53 +1,38 @@ package oidc import ( - "io/fs" "net/http" - "strings" "github.com/flanksource/duty/context" - "github.com/flanksource/incident-commander/auth/oidc/static" "github.com/labstack/echo/v4" + "github.com/tg123/go-htpasswd" ) // MountRoutes sets up OIDC endpoints on the echo server. -// The OIDC provider is mounted under /oidc/ with the issuer set to {issuerURL}/oidc -// so that discovery at /oidc/.well-known/openid-configuration returns correct endpoint URLs. -// A convenience redirect from /.well-known/openid-configuration is provided for standard discovery. -func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath string, checker CredentialChecker, lookup PersonLookup) error { - oidcIssuer := strings.TrimRight(issuerURL, "/") - provider, err := NewProvider(ctx, oidcIssuer, signingKeyPath) +// Called from auth.Middleware when OIDC is enabled. +func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath, htpasswdFile string, lookup personLookup) error { + provider, err := NewProvider(ctx, issuerURL, signingKeyPath) if err != nil { return err } - loginHandler := NewLoginHandler(provider.Storage, provider.OpenIDProvider, checker, lookup, oidcIssuer) - // Custom login form (not part of the standard OIDC protocol paths). + checker, err := htpasswd.New(htpasswdFile, htpasswd.DefaultSystems, nil) + if err != nil { + return err + } + + loginHandler := NewLoginHandler(provider.Storage, provider.OpenIDProvider, checker, lookup) + e.GET("/oidc/login", loginHandler.ShowForm) e.POST("/oidc/login", loginHandler.HandleSubmit) - e.GET("/oidc/kratos/callback", loginHandler.HandleExternalCallback) - - // MCP Clients need OAuth well-known discovery endpoints (not just OIDC discovery). - mountOAuthRoutes(e, oidcIssuer) - - // Standard OIDC protocol endpoints — mounted at the root so that the issuer URL - // and the authorization_endpoint/token_endpoint values in the discovery document - // resolve to real routes on this server. - h := echo.WrapHandler(provider.Handler) - e.Any("/authorize", h) - e.Any("/authorize/*", h) - e.Any("/oauth/token", h) - e.Any("/oauth/introspect", h) - e.Any("/userinfo", h) - e.Any("/keys", h) - e.Any("/endsession", h) - e.Any("/.well-known/*", h) - - // Serve embedded static assets (logo, tailwind) - staticFS, _ := fs.Sub(static.FS, ".") - staticHandler := http.StripPrefix("/oidc/static/", http.FileServer(http.FS(staticFS))) - e.GET("/oidc/static/*", echo.WrapHandler(staticHandler)) + + // Mount the zitadel OP handler under /oidc (handles authorize, token, userinfo, etc.) + oidcHandler := http.StripPrefix("/oidc", provider.Handler) + e.Any("/oidc/*", echo.WrapHandler(oidcHandler)) + + // Well-known endpoints (discovery, jwks) + e.Any("/.well-known/*", echo.WrapHandler(provider.Handler)) return nil } diff --git a/auth/oidc/storage.go b/auth/oidc/storage.go index 476fefbf3..2ca5e0676 100644 --- a/auth/oidc/storage.go +++ b/auth/oidc/storage.go @@ -4,9 +4,7 @@ import ( gocontext "context" "crypto" "crypto/rsa" - "crypto/sha256" "crypto/x509" - "encoding/base64" "encoding/json" "encoding/pem" "fmt" @@ -29,7 +27,7 @@ type signingKey struct { func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { return s.algorithm } func (s *signingKey) Key() any { return s.privateKey } -func (s *signingKey) ID() string { return s.id } +func (s *signingKey) ID() string { return s.id } type publicKey struct { id string @@ -37,10 +35,10 @@ type publicKey struct { key *rsa.PublicKey } -func (p *publicKey) ID() string { return p.id } -func (p *publicKey) Algorithm() jose.SignatureAlgorithm { return p.algorithm } -func (p *publicKey) Use() string { return "sig" } -func (p *publicKey) Key() any { return p.key } +func (p *publicKey) ID() string { return p.id } +func (p *publicKey) Algorithm() jose.SignatureAlgorithm { return p.algorithm } +func (p *publicKey) Use() string { return "sig" } +func (p *publicKey) Key() any { return p.key } // Storage implements op.Storage backed by Postgres. type Storage struct { @@ -76,6 +74,7 @@ func (s *Storage) CreateAuthRequest(_ gocontext.Context, req *oidc.AuthRequest, return ar, nil } + func (s *Storage) AuthRequestByID(_ gocontext.Context, id string) (op.AuthRequest, error) { var ar AuthRequest if err := s.ctx.DB().Where("id = ? AND expires_at > NOW()", id).First(&ar).Error; err != nil { @@ -86,7 +85,7 @@ func (s *Storage) AuthRequestByID(_ gocontext.Context, id string) (op.AuthReques func (s *Storage) AuthRequestByCode(_ gocontext.Context, code string) (op.AuthRequest, error) { var ar AuthRequest - if err := s.ctx.DB().Where("code = ? AND expires_at > NOW()", hashToken(code)).First(&ar).Error; err != nil { + if err := s.ctx.DB().Where("code = ? AND expires_at > NOW()", code).First(&ar).Error; err != nil { return nil, fmt.Errorf("auth request not found: %w", err) } return &ar, nil @@ -94,7 +93,7 @@ func (s *Storage) AuthRequestByCode(_ gocontext.Context, code string) (op.AuthRe func (s *Storage) SaveAuthCode(_ gocontext.Context, id, code string) error { return s.ctx.DB().Model(&AuthRequest{}).Where("id = ?", id). - Updates(map[string]any{"code": hashToken(code), "done": true}).Error + Updates(map[string]any{"code": code, "done": true}).Error } func (s *Storage) DeleteAuthRequest(_ gocontext.Context, id string) error { @@ -114,28 +113,26 @@ func (s *Storage) CreateAccessAndRefreshTokens(_ gocontext.Context, req op.Token if currentRefreshToken != "" { // find existing rotation family var existing RefreshToken - if err := s.ctx.DB().Where("token = ?", hashToken(currentRefreshToken)).First(&existing).Error; err == nil { + if err := s.ctx.DB().Where("token = ?", currentRefreshToken).First(&existing).Error; err == nil { rotationID = existing.RotationID // rotate: mark old token expired - s.ctx.DB().Model(&RefreshToken{}).Where("token = ?", hashToken(currentRefreshToken)). + s.ctx.DB().Model(&RefreshToken{}).Where("token = ?", currentRefreshToken). Update("expires_at", time.Now()) } } - now := time.Now() - clientID := ClientID - if aud := req.GetAudience(); len(aud) > 0 { - clientID = aud[0] + ar, ok := req.(*AuthRequest) + if !ok { + return "", "", time.Time{}, fmt.Errorf("unexpected request type %T", req) } - rawRefreshToken := uuid.New().String() - + now := time.Now() rt := &RefreshToken{ ID: uuid.New().String(), - Token: hashToken(rawRefreshToken), - ClientID: clientID, - Subject: req.GetSubject(), - Scopes: StringList(req.GetScopes()), + Token: uuid.New().String(), + ClientID: ar.ClientID, + Subject: ar.Subject, + Scopes: ar.Scopes, AuthTime: now, RotationID: rotationID, CreatedAt: now, @@ -145,12 +142,12 @@ func (s *Storage) CreateAccessAndRefreshTokens(_ gocontext.Context, req op.Token return "", "", time.Time{}, fmt.Errorf("create refresh token: %w", err) } - return accessTokenID, rawRefreshToken, expiry, nil + return accessTokenID, rt.Token, expiry, nil } func (s *Storage) TokenRequestByRefreshToken(_ gocontext.Context, refreshToken string) (op.RefreshTokenRequest, error) { var rt RefreshToken - if err := s.ctx.DB().Where("token = ? AND expires_at > NOW()", hashToken(refreshToken)).First(&rt).Error; err != nil { + if err := s.ctx.DB().Where("token = ? AND expires_at > NOW()", refreshToken).First(&rt).Error; err != nil { return nil, op.ErrInvalidRefreshToken } return &rt, nil @@ -165,7 +162,7 @@ func (s *Storage) RevokeToken(_ gocontext.Context, tokenOrID, userID, _ string) if userID != "" { query = s.ctx.DB().Where("id = ? AND subject = ?", tokenOrID, userID) } else { - query = s.ctx.DB().Where("id = ? OR token = ?", tokenOrID, hashToken(tokenOrID)) + query = s.ctx.DB().Where("token = ?", tokenOrID) } if err := query.Delete(&RefreshToken{}).Error; err != nil { return oidc.ErrServerError() @@ -175,7 +172,7 @@ func (s *Storage) RevokeToken(_ gocontext.Context, tokenOrID, userID, _ string) func (s *Storage) GetRefreshTokenInfo(_ gocontext.Context, _, token string) (string, string, error) { var rt RefreshToken - if err := s.ctx.DB().Where("token = ?", hashToken(token)).First(&rt).Error; err != nil { + if err := s.ctx.DB().Where("token = ?", token).First(&rt).Error; err != nil { return "", "", op.ErrInvalidRefreshToken } return rt.Subject, rt.ID, nil @@ -265,7 +262,6 @@ func (s *Storage) SetAuthRequestSubject(id, subject string) error { Updates(map[string]any{ "subject": subject, "auth_time": now, - "done": true, }).Error } @@ -305,7 +301,3 @@ func generateKeyID(pub *rsa.PublicKey) (string, error) { return fmt.Sprintf("%x", h.Sum(nil))[:16], nil } -func hashToken(token string) string { - h := sha256.Sum256([]byte(token)) - return base64.RawURLEncoding.EncodeToString(h[:]) -} diff --git a/auth/oidc_test.go b/auth/oidc_test.go index 670f82eda..d798a7dad 100644 --- a/auth/oidc_test.go +++ b/auth/oidc_test.go @@ -52,8 +52,6 @@ var _ = ginkgo.Describe("OIDC", func() { ginkgo.AfterEach(func() { DefaultContext.DB().Where("id = ?", person.ID).Delete(&models.Person{}) - DefaultContext.DB().Exec("DELETE FROM oidc_auth_requests") - DefaultContext.DB().Exec("DELETE FROM oidc_refresh_tokens") }) ginkgo.It("creates signing key file on first start", func() { @@ -270,12 +268,13 @@ var _ = ginkgo.Describe("OIDC", func() { // Clear the cache so it reloads from DB oidcPublicKeyCache.Flush() - savedPublicURL := api.FrontendURL - api.FrontendURL = "http://localhost:8080" - defer func() { api.FrontendURL = savedPublicURL }() + issuer := "http://localhost:8080" + savedPublicURL := api.PublicURL + api.PublicURL = issuer + defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080", + "iss": issuer, "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -296,9 +295,9 @@ var _ = ginkgo.Describe("OIDC", func() { Expect(err).ToNot(HaveOccurred()) oidcPublicKeyCache.Flush() - savedPublicURL := api.FrontendURL - api.FrontendURL = "http://localhost:8080" - defer func() { api.FrontendURL = savedPublicURL }() + savedPublicURL := api.PublicURL + api.PublicURL = "http://localhost:8080" + defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ "iss": "http://localhost:8080", @@ -323,9 +322,9 @@ var _ = ginkgo.Describe("OIDC", func() { Expect(err).ToNot(HaveOccurred()) oidcPublicKeyCache.Flush() - savedPublicURL := api.FrontendURL - api.FrontendURL = "http://localhost:8080" - defer func() { api.FrontendURL = savedPublicURL }() + savedPublicURL := api.PublicURL + api.PublicURL = "http://localhost:8080" + defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ "iss": "http://localhost:8080", @@ -350,9 +349,9 @@ var _ = ginkgo.Describe("OIDC", func() { Expect(err).ToNot(HaveOccurred()) oidcPublicKeyCache.Flush() - savedPublicURL := api.FrontendURL - api.FrontendURL = "http://localhost:8080" - defer func() { api.FrontendURL = savedPublicURL }() + savedPublicURL := api.PublicURL + api.PublicURL = "http://localhost:8080" + defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ "iss": "http://localhost:8080", @@ -377,9 +376,9 @@ var _ = ginkgo.Describe("OIDC", func() { Expect(err).ToNot(HaveOccurred()) oidcPublicKeyCache.Flush() - savedPublicURL := api.FrontendURL - api.FrontendURL = "http://localhost:8080" - defer func() { api.FrontendURL = savedPublicURL }() + savedPublicURL := api.PublicURL + api.PublicURL = "http://localhost:8080" + defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ "iss": "http://localhost:8080", @@ -400,7 +399,7 @@ var _ = ginkgo.Describe("OIDC", func() { ginkgo.Describe("LoginHandler", func() { ginkgo.It("renders login form with auth_request_id", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080") + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup) e := newEchoInstance(DefaultContext) req := httptest.NewRequest(http.MethodGet, "/oidc/login?auth_request_id=test-123", nil) req = req.WithContext(DefaultContext.Wrap(req.Context())) @@ -414,7 +413,7 @@ var _ = ginkgo.Describe("OIDC", func() { }) ginkgo.It("returns 400 when auth_request_id is missing", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080") + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup) e := newEchoInstance(DefaultContext) req := httptest.NewRequest(http.MethodGet, "/oidc/login", nil) req = req.WithContext(DefaultContext.Wrap(req.Context())) @@ -425,47 +424,8 @@ var _ = ginkgo.Describe("OIDC", func() { Expect(rec.Code).To(Equal(http.StatusBadRequest)) }) - ginkgo.It("redirects to external login when checker supports login redirect", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &redirectChecker{}, mockLookup, "http://localhost:8080") - e := newEchoInstance(DefaultContext) - req := httptest.NewRequest(http.MethodGet, "/oidc/login?auth_request_id=req-123", nil) - req = req.WithContext(DefaultContext.Wrap(req.Context())) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - Expect(login.ShowForm(c)).To(Succeed()) - Expect(rec.Code).To(Equal(http.StatusFound)) - Expect(rec.Header().Get("Location")).To(Equal("http://localhost:3000/login?return_to=%2Foidc%2Fkratos%2Fcallback%3Fauth_request_id%3Dreq-123")) - }) - - ginkgo.It("completes auth request from external callback", func() { - req := &oidclib.AuthRequest{ - ClientID: oidc.ClientID, - RedirectURI: "http://localhost:9999/callback", - Scopes: []string{"openid"}, - ResponseType: "code", - } - ar, err := provider.Storage.CreateAuthRequest(gocontext.TODO(), req, "") - Expect(err).ToNot(HaveOccurred()) - - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &callbackChecker{subjectID: person.ID.String()}, mockLookup, "http://localhost:8080") - e := newEchoInstance(DefaultContext) - httpReq := httptest.NewRequest(http.MethodGet, "/oidc/kratos/callback?auth_request_id="+ar.GetID(), nil) - httpReq = httpReq.WithContext(DefaultContext.Wrap(httpReq.Context())) - rec := httptest.NewRecorder() - c := e.NewContext(httpReq, rec) - - Expect(login.HandleExternalCallback(c)).To(Succeed()) - Expect(rec.Code).To(Equal(http.StatusFound)) - Expect(rec.Header().Get("Location")).To(ContainSubstring("id=" + ar.GetID())) - - updated, err := provider.Storage.AuthRequestByID(gocontext.TODO(), ar.GetID()) - Expect(err).ToNot(HaveOccurred()) - Expect(updated.GetSubject()).To(Equal(person.ID.String())) - }) - ginkgo.It("rejects invalid credentials", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{valid: false}, mockLookup, "http://localhost:8080") + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{valid: false}, mockLookup) e := newEchoInstance(DefaultContext) form := url.Values{ @@ -586,34 +546,7 @@ type mockChecker struct { valid bool } -func (m *mockChecker) Match(_ dutyContext.Context, _, _ string) error { - if m.valid { - return nil - } - return fmt.Errorf("invalid credentials") -} - -type redirectChecker struct{} - -func (r *redirectChecker) Match(_ dutyContext.Context, _, _ string) error { - return nil -} - -func (r *redirectChecker) LoginRedirectURL(authRequestID string) (string, error) { - return "http://localhost:3000/login?return_to=%2Foidc%2Fkratos%2Fcallback%3Fauth_request_id%3D" + authRequestID, nil -} - -type callbackChecker struct { - subjectID string -} - -func (r *callbackChecker) Match(_ dutyContext.Context, _, _ string) error { - return nil -} - -func (r *callbackChecker) CallbackSubject(_ echo.Context) (string, error) { - return r.subjectID, nil -} +func (m *mockChecker) Match(_, _ string) bool { return m.valid } var mockLookup = func(ctx dutyContext.Context, user string) (string, error) { return uuid.New().String(), nil diff --git a/auth/oidc_validate.go b/auth/oidc_validate.go index 3ca910140..1e69406a8 100644 --- a/auth/oidc_validate.go +++ b/auth/oidc_validate.go @@ -35,7 +35,7 @@ func authenticateOIDCToken(c echo.Context, tokenStr string) (bool, error) { return false, nil } - issuer := strings.TrimRight(api.FrontendURL, "/") + issuer := api.PublicURL var lastErr error for _, pub := range keys { @@ -66,7 +66,7 @@ func authenticateOIDCToken(c echo.Context, tokenStr string) (bool, error) { if auds, ok := claims["aud"].([]any); ok { found := false for _, a := range auds { - if audStr, ok := a.(string); ok && audStr == oidcmodels.ClientID { + if a.(string) == oidcmodels.ClientID { found = true break } diff --git a/cmd/auth_login.go b/cmd/auth_login.go index 10b54ddc2..5d9ab46a3 100644 --- a/cmd/auth_login.go +++ b/cmd/auth_login.go @@ -2,6 +2,9 @@ package cmd import ( "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" "net" @@ -14,59 +17,61 @@ import ( "strings" "time" - "github.com/flanksource/incident-commander/auth/oidc/static" - "github.com/flanksource/incident-commander/auth/oidcclient" "github.com/spf13/cobra" ) +var AuthCmd = &cobra.Command{ + Use: "auth", + Short: "Authentication commands", +} + var authLoginCmd = &cobra.Command{ Use: "login", Short: "Log in via OIDC browser flow", RunE: runAuthLogin, } -var ( - loginServer string - loginPrintToken bool -) +var loginServer string func init() { authLoginCmd.Flags().StringVar(&loginServer, "server", "", "Mission Control server URL (required)") - authLoginCmd.Flags().BoolVar(&loginPrintToken, "print-token", false, "Print access and refresh tokens to stdout") _ = authLoginCmd.MarkFlagRequired("server") - Auth.AddCommand(authLoginCmd) + AuthCmd.AddCommand(authLoginCmd) + Root.AddCommand(AuthCmd) +} + +type oidcTokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IDToken string `json:"id_token"` + ExpiresAt time.Time `json:"expires_at"` } func runAuthLogin(cmd *cobra.Command, _ []string) error { serverURL := strings.TrimRight(loginServer, "/") - endpoints, err := oidcclient.Discover(serverURL + "/.well-known/openid-configuration") + // Discover OIDC endpoints + discoveryURL := serverURL + "/.well-known/openid-configuration" + endpoints, err := discoverOIDC(discoveryURL) if err != nil { return fmt.Errorf("OIDC discovery failed: %w", err) } - verifier, challenge, err := oidcclient.GeneratePKCE() + // Generate PKCE values + verifier, challenge, err := generatePKCE() if err != nil { return fmt.Errorf("PKCE generation failed: %w", err) } - state, err := oidcclient.RandomBase64(16) - if err != nil { - return fmt.Errorf("state generation failed: %w", err) - } - nonce, err := oidcclient.RandomBase64(16) - if err != nil { - return fmt.Errorf("nonce generation failed: %w", err) - } + state := randomBase64(16) + nonce := randomBase64(16) + // Start local callback server listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return fmt.Errorf("failed to start local server: %w", err) } redirectURI := fmt.Sprintf("http://127.0.0.1:%d/callback", listener.Addr().(*net.TCPAddr).Port) - // Render the success page with absolute URLs to the MC server's static assets - successHTML := strings.ReplaceAll(static.CallbackSuccessHTML, "/oidc/static/", serverURL+"/oidc/static/") - codeCh := make(chan string, 1) errCh := make(chan error, 1) server := &http.Server{} @@ -93,8 +98,7 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { errCh <- fmt.Errorf("missing authorization code") return } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprint(w, successHTML) + fmt.Fprintf(w, "

Login successful!

You can close this tab.

") codeCh <- code }) @@ -105,6 +109,7 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { }() defer func() { _ = server.Shutdown(context.Background()) }() + // Build authorize URL authURL := fmt.Sprintf("%s?client_id=mc-cli&response_type=code&scope=%s&redirect_uri=%s&state=%s&nonce=%s&code_challenge=%s&code_challenge_method=S256", endpoints.AuthorizationEndpoint, url.QueryEscape("openid profile email offline_access"), @@ -117,6 +122,7 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { fmt.Fprintf(cmd.OutOrStdout(), "Opening browser for login...\n%s\n\n", authURL) openBrowser(authURL) + // Wait for callback var code string select { case code = <-codeCh: @@ -126,93 +132,152 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { return fmt.Errorf("login timed out") } - tokens, err := oidcclient.ExchangeCode(endpoints.TokenEndpoint, code, redirectURI, verifier) + // Exchange code for tokens + tokens, err := exchangeCode(endpoints.TokenEndpoint, code, redirectURI, verifier) if err != nil { return fmt.Errorf("token exchange failed: %w", err) } - if err := oidcclient.ValidateNonce(tokens.IDToken, nonce); err != nil { + // Validate nonce in ID token + if err := validateNonce(tokens.IDToken, nonce); err != nil { return fmt.Errorf("nonce validation failed: %w", err) } - tokenPath, err := storeTokens(serverURL, tokens) + // Store tokens + if err := storeTokens(serverURL, tokens); err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "warning: could not save tokens: %v\n", err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "\nLogin successful!\n\n") + fmt.Fprintf(cmd.OutOrStdout(), "Access token:\n%s\n\n", tokens.AccessToken) + fmt.Fprintf(cmd.OutOrStdout(), "Usage example:\n curl -H \"Authorization: Bearer %s\" %s/auth/whoami\n", tokens.AccessToken, serverURL) + + return nil +} + +type oidcDiscovery struct { + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserinfoEndpoint string `json:"userinfo_endpoint"` +} + +func discoverOIDC(discoveryURL string) (*oidcDiscovery, error) { + resp, err := http.Get(discoveryURL) if err != nil { - return fmt.Errorf("failed to save tokens: %w", err) + return nil, err + } + defer resp.Body.Close() + + var d oidcDiscovery + if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { + return nil, err } + return &d, nil +} - if err := saveContextFromLogin(serverURL, tokens.AccessToken); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to save context: %v\n", err) +func generatePKCE() (verifier, challenge string, err error) { + b := make([]byte, 32) + if _, err = rand.Read(b); err != nil { + return } + verifier = base64.RawURLEncoding.EncodeToString(b) + h := sha256.Sum256([]byte(verifier)) + challenge = base64.RawURLEncoding.EncodeToString(h[:]) + return +} - fmt.Fprintf(cmd.OutOrStdout(), "\nLogin successful!\n\n") - fmt.Fprintf(cmd.OutOrStdout(), "Tokens saved to: %s\n\n", tokenPath) +func randomBase64(n int) string { + b := make([]byte, n) + _, _ = rand.Read(b) + return base64.RawURLEncoding.EncodeToString(b) +} - if loginPrintToken { - fmt.Fprintf(cmd.OutOrStdout(), "Access token (expires %s):\n%s\n\n", tokens.ExpiresAt.Format("15:04:05"), tokens.AccessToken) - fmt.Fprintf(cmd.OutOrStdout(), "Refresh token:\n%s\n\n", tokens.RefreshToken) - fmt.Fprintf(cmd.OutOrStdout(), "curl -H 'Authorization: Bearer %s' %s/whoami\n\n", tokens.AccessToken, serverURL) - } else { - fmt.Fprintf(cmd.OutOrStdout(), "Use --print-token to print tokens to stdout.\n") - fmt.Fprintf(cmd.OutOrStdout(), "curl -H 'Authorization: Bearer ' %s/whoami\n\n", serverURL) +func exchangeCode(tokenEndpoint, code, redirectURI, verifier string) (*oidcTokens, error) { + form := url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "redirect_uri": {redirectURI}, + "client_id": {"mc-cli"}, + "code_verifier": {verifier}, } - return nil + resp, err := http.PostForm(tokenEndpoint, form) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("token endpoint returned %d", resp.StatusCode) + } + + var result struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IDToken string `json:"id_token"` + ExpiresIn int `json:"expires_in"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &oidcTokens{ + AccessToken: result.AccessToken, + RefreshToken: result.RefreshToken, + IDToken: result.IDToken, + ExpiresAt: time.Now().Add(time.Duration(result.ExpiresIn) * time.Second), + }, nil } -func saveContextFromLogin(serverURL, accessToken string) error { - cfg, err := LoadConfig() +// validateNonce extracts the nonce claim from an ID token (without full verification). +func validateNonce(idToken, expectedNonce string) error { + parts := strings.Split(idToken, ".") + if len(parts) != 3 { + return fmt.Errorf("invalid ID token format") + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { - return err + return fmt.Errorf("decode ID token payload: %w", err) } - name := ServerToContextName(serverURL) - existing := cfg.GetContext(name) - ctx := MCContext{ - Name: name, - Server: serverURL, - Token: accessToken, + var claims map[string]any + if err := json.Unmarshal(payload, &claims); err != nil { + return fmt.Errorf("parse ID token claims: %w", err) } - if existing != nil { - ctx.DB = existing.DB - ctx.Properties = existing.Properties + nonce, _ := claims["nonce"].(string) + if nonce != expectedNonce { + return fmt.Errorf("nonce mismatch") } - cfg.SetContext(ctx) - cfg.CurrentContext = name - return SaveConfig(cfg) + return nil } -func storeTokens(serverURL string, tokens *oidcclient.Tokens) (string, error) { +func storeTokens(serverURL string, tokens *oidcTokens) error { dir, err := os.UserConfigDir() if err != nil { - return "", err + return err } dir = filepath.Join(dir, "mission-control") if err := os.MkdirAll(dir, 0700); err != nil { - return "", err + return err } + // Store per server host := strings.NewReplacer("://", "_", "/", "_", ":", "_").Replace(serverURL) path := filepath.Join(dir, fmt.Sprintf("tokens_%s.json", host)) data, err := json.MarshalIndent(tokens, "", " ") if err != nil { - return "", err + return err } - return path, os.WriteFile(path, data, 0600) + return os.WriteFile(path, data, 0600) } func openBrowser(url string) { var cmd string - var args []string switch runtime.GOOS { case "darwin": cmd = "open" - args = []string{url} - case "windows": - cmd = "cmd" - args = []string{"/c", "start", url} default: cmd = "xdg-open" - args = []string{url} } - _ = exec.Command(cmd, args...).Start() + _ = exec.Command(cmd, url).Start() } diff --git a/go.mod b/go.mod index 8b7d58612..b4f174045 100644 --- a/go.mod +++ b/go.mod @@ -440,8 +440,8 @@ require ( golang.org/x/crypto v0.49.0 golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 golang.org/x/time v0.14.0 // indirect google.golang.org/api v0.262.0 google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed // indirect diff --git a/jobs/jobs.go b/jobs/jobs.go index e8ee4d712..40339c4ed 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -111,7 +111,7 @@ func Start(ctx context.Context, mcpServer *server.MCPServer) { } if err := notification.InitCRDStatusUpdates(ctx); err != nil { - logger.Errorf("failed to start notification status update queue: %v", err) + logger.Errorf("failed to start notificatino status update queue: %v", err) } } From e1e69bf90615989eee558414ad044162761b4a4e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 20 Mar 2026 09:14:07 +0200 Subject: [PATCH 02/48] style(auth): apply gofmt formatting --- auth/basic.go | 4 ++-- auth/oidc/login.go | 6 +++--- auth/oidc/models.go | 19 +++++++++---------- auth/oidc/routes.go | 1 - auth/oidc/storage.go | 12 +++++------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/auth/basic.go b/auth/basic.go index 5e7705ea6..8e55e3778 100644 --- a/auth/basic.go +++ b/auth/basic.go @@ -19,8 +19,8 @@ import ( ) var ( - HtpasswdFile string - OIDCEnabled bool + HtpasswdFile string + OIDCEnabled bool OIDCSigningKeyPath string checker *htpasswd.File diff --git a/auth/oidc/login.go b/auth/oidc/login.go index 418773e78..b205980f6 100644 --- a/auth/oidc/login.go +++ b/auth/oidc/login.go @@ -27,9 +27,9 @@ const loginFormHTML = ` // LoginHandler handles the OIDC login form, delegating credential validation // to the Basic auth checker and person lookup. type LoginHandler struct { - storage *Storage - provider op.OpenIDProvider - checker credentialChecker + storage *Storage + provider op.OpenIDProvider + checker credentialChecker personLookup personLookup } diff --git a/auth/oidc/models.go b/auth/oidc/models.go index 0bfc0b3cc..e0a1f0fe5 100644 --- a/auth/oidc/models.go +++ b/auth/oidc/models.go @@ -62,16 +62,16 @@ type AuthRequest struct { func (AuthRequest) TableName() string { return "oidc_auth_requests" } func (a *AuthRequest) GetID() string { return a.ID } -func (a *AuthRequest) GetACR() string { return "" } -func (a *AuthRequest) GetAMR() []string { return nil } -func (a *AuthRequest) GetAudience() []string { return []string{a.ClientID} } +func (a *AuthRequest) GetACR() string { return "" } +func (a *AuthRequest) GetAMR() []string { return nil } +func (a *AuthRequest) GetAudience() []string { return []string{a.ClientID} } func (a *AuthRequest) GetAuthTime() time.Time { if a.AuthTime != nil { return *a.AuthTime } return time.Time{} } -func (a *AuthRequest) GetClientID() string { return a.ClientID } +func (a *AuthRequest) GetClientID() string { return a.ClientID } func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { if a.CodeChallenge == "" { return nil @@ -87,10 +87,10 @@ func (a *AuthRequest) GetResponseType() oidc.ResponseType { return oidc.ResponseType(a.ResponseType) } func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { return "" } -func (a *AuthRequest) GetScopes() []string { return []string(a.Scopes) } -func (a *AuthRequest) GetState() string { return a.State } -func (a *AuthRequest) GetSubject() string { return a.Subject } -func (a *AuthRequest) Done() bool { return a.IsDone } +func (a *AuthRequest) GetScopes() []string { return []string(a.Scopes) } +func (a *AuthRequest) GetState() string { return a.State } +func (a *AuthRequest) GetSubject() string { return a.Subject } +func (a *AuthRequest) Done() bool { return a.IsDone } // RefreshToken is backed by the oidc_refresh_tokens table. type RefreshToken struct { @@ -165,7 +165,6 @@ func (c *cliClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []st func (c *cliClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { return nil } -func (c *cliClient) IsScopeAllowed(scope string) bool { return true } +func (c *cliClient) IsScopeAllowed(scope string) bool { return true } func (c *cliClient) IDTokenUserinfoClaimsAssertion() bool { return false } func (c *cliClient) ClockSkew() time.Duration { return 0 } - diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index 9c12edd08..fd99f6230 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -16,7 +16,6 @@ func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath, h return err } - checker, err := htpasswd.New(htpasswdFile, htpasswd.DefaultSystems, nil) if err != nil { return err diff --git a/auth/oidc/storage.go b/auth/oidc/storage.go index 2ca5e0676..17766afc9 100644 --- a/auth/oidc/storage.go +++ b/auth/oidc/storage.go @@ -27,7 +27,7 @@ type signingKey struct { func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { return s.algorithm } func (s *signingKey) Key() any { return s.privateKey } -func (s *signingKey) ID() string { return s.id } +func (s *signingKey) ID() string { return s.id } type publicKey struct { id string @@ -35,10 +35,10 @@ type publicKey struct { key *rsa.PublicKey } -func (p *publicKey) ID() string { return p.id } -func (p *publicKey) Algorithm() jose.SignatureAlgorithm { return p.algorithm } -func (p *publicKey) Use() string { return "sig" } -func (p *publicKey) Key() any { return p.key } +func (p *publicKey) ID() string { return p.id } +func (p *publicKey) Algorithm() jose.SignatureAlgorithm { return p.algorithm } +func (p *publicKey) Use() string { return "sig" } +func (p *publicKey) Key() any { return p.key } // Storage implements op.Storage backed by Postgres. type Storage struct { @@ -74,7 +74,6 @@ func (s *Storage) CreateAuthRequest(_ gocontext.Context, req *oidc.AuthRequest, return ar, nil } - func (s *Storage) AuthRequestByID(_ gocontext.Context, id string) (op.AuthRequest, error) { var ar AuthRequest if err := s.ctx.DB().Where("id = ? AND expires_at > NOW()", id).First(&ar).Error; err != nil { @@ -300,4 +299,3 @@ func generateKeyID(pub *rsa.PublicKey) (string, error) { h.Write(b) return fmt.Sprintf("%x", h.Sum(nil))[:16], nil } - From eb65bfd4dead313d74bb4ca0680ebe10053cc95e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 20 Mar 2026 10:05:58 +0200 Subject: [PATCH 03/48] feat(auth): add OIDC support for Kratos auth mode - Export CredentialChecker and PersonLookup interfaces from oidc package - Decouple MountRoutes from htpasswd: accept CredentialChecker directly - Add KratosCredentialChecker that validates credentials via Kratos API - Add LookupKratosPersonByUsername for Kratos identity-to-person mapping - Add OIDC Bearer token validation to Kratos Session middleware - Fix unique constraint violation: use *string for AuthRequest.Code - Clean up OIDC test data in AfterEach to prevent constraint conflicts --- auth/basic.go | 17 +++++++++++++++++ auth/kratos.go | 40 +++------------------------------------- auth/middleware.go | 7 ++----- auth/oidc/login.go | 18 +++++++++--------- auth/oidc/models.go | 2 +- auth/oidc/routes.go | 11 +---------- auth/oidc_test.go | 2 ++ 7 files changed, 35 insertions(+), 62 deletions(-) diff --git a/auth/basic.go b/auth/basic.go index 8e55e3778..37a97eb5f 100644 --- a/auth/basic.go +++ b/auth/basic.go @@ -194,6 +194,23 @@ func LookupPersonByUsername(ctx context.Context, username string) (string, error return person.ID.String(), nil } +// HtpasswdChecker wraps htpasswd.File to implement oidc.CredentialChecker. +type HtpasswdChecker struct { + file *htpasswd.File +} + +func NewHtpasswdChecker(path string) (*HtpasswdChecker, error) { + f, err := htpasswd.New(path, htpasswd.DefaultSystems, nil) + if err != nil { + return nil, err + } + return &HtpasswdChecker{file: f}, nil +} + +func (h *HtpasswdChecker) Match(user, pass string) bool { + return h.file.Match(user, pass) +} + func lookupPerson(ctx context.Context, user string) (*models.Person, error) { user = strings.ToLower(user) var person models.Person diff --git a/auth/kratos.go b/auth/kratos.go index c6859d8b5..d8914bf68 100644 --- a/auth/kratos.go +++ b/auth/kratos.go @@ -241,43 +241,9 @@ func NewKratosCredentialChecker(m *kratosMiddleware) *KratosCredentialChecker { return &KratosCredentialChecker{middleware: m} } -func (k *KratosCredentialChecker) Match(ctx context.Context, user, pass string) error { - _, err := k.middleware.kratosLoginWithCache(ctx, user, pass) - return err -} - -func (k *KratosCredentialChecker) LoginRedirectURL(authRequestID string) (string, error) { - frontendURL := strings.TrimRight(incAPI.FrontendURL, "/") - if frontendURL == "" { - return "", fmt.Errorf("frontend URL is not configured") - } - - q := url.Values{} - q.Set("return_to", "/oidc/kratos/callback?auth_request_id="+authRequestID) - return frontendURL + "/login?" + q.Encode(), nil -} - -func (k *KratosCredentialChecker) CallbackSubject(c echo.Context) (string, error) { - ctx := c.Request().Context().(context.Context) - - session, err := k.middleware.validateSession(ctx, c.Request()) - if err != nil { - return "", err - } - if session.Active == nil || !*session.Active { - return "", fmt.Errorf("session is not active") - } - - subject := session.Identity.GetId() - if subject == "" { - return "", fmt.Errorf("session identity is missing") - } - - if _, err := db.GetUserByID(ctx, subject); err != nil { - return "", fmt.Errorf("failed to resolve user: %w", err) - } - - return subject, nil +func (k *KratosCredentialChecker) Match(user, pass string) bool { + _, err := k.middleware.kratosLoginWithCache(gocontext.Background(), user, pass) + return err == nil } // LookupKratosPersonByUsername finds a person by email in the Kratos identities table. diff --git a/auth/middleware.go b/auth/middleware.go index de75338dc..a3f1a0625 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -77,9 +77,6 @@ func Middleware(ctx context.Context, e *echo.Echo) error { switch vars.AuthMode { case Basic: - if OIDCEnabled && (vars.AuthMode == Kratos || vars.AuthMode == Clerk) { - return fmt.Errorf("--oidc is only supported with --auth basic") - } UseBasic(e) if admin, err := GetOrCreateAdminUser(ctx); err != nil { return fmt.Errorf("failed to created admin user: %v", err) @@ -116,10 +113,10 @@ func Middleware(ctx context.Context, e *echo.Echo) error { if OIDCEnabled { kratosChecker := NewKratosCredentialChecker(kratosMiddleware) - if err := oidc.MountRoutes(e, ctx, api.FrontendURL, OIDCSigningKeyPath, kratosChecker, LookupKratosPersonByUsername); err != nil { + if err := oidc.MountRoutes(e, ctx, api.PublicURL, OIDCSigningKeyPath, kratosChecker, LookupKratosPersonByUsername); err != nil { return fmt.Errorf("failed to mount OIDC routes: %w", err) } - logger.Infof("OIDC provider enabled at %s (Kratos auth)", api.FrontendURL) + logger.Infof("OIDC provider enabled at %s (Kratos auth)", api.PublicURL) } case Clerk: diff --git a/auth/oidc/login.go b/auth/oidc/login.go index b205980f6..1c1258d79 100644 --- a/auth/oidc/login.go +++ b/auth/oidc/login.go @@ -29,24 +29,24 @@ const loginFormHTML = ` type LoginHandler struct { storage *Storage provider op.OpenIDProvider - checker credentialChecker - personLookup personLookup + checker CredentialChecker + PersonLookup PersonLookup } -// credentialChecker validates username/password via htpasswd. -type credentialChecker interface { +// CredentialChecker validates username/password credentials. +type CredentialChecker interface { Match(user, pass string) bool } -// personLookup finds a person by username/email, returning the person UUID. -type personLookup func(ctx context.Context, user string) (personID string, err error) +// PersonLookup finds a person by username/email, returning the person UUID. +type PersonLookup func(ctx context.Context, user string) (personID string, err error) -func NewLoginHandler(storage *Storage, provider op.OpenIDProvider, checker credentialChecker, lookup personLookup) *LoginHandler { +func NewLoginHandler(storage *Storage, provider op.OpenIDProvider, checker CredentialChecker, lookup PersonLookup) *LoginHandler { return &LoginHandler{ storage: storage, provider: provider, checker: checker, - personLookup: lookup, + PersonLookup: lookup, } } @@ -77,7 +77,7 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { return renderForm("Invalid credentials") } - personID, err := h.personLookup(ctx, username) + personID, err := h.PersonLookup(ctx, username) if err != nil { return renderForm("User not found") } diff --git a/auth/oidc/models.go b/auth/oidc/models.go index e0a1f0fe5..2325a36f4 100644 --- a/auth/oidc/models.go +++ b/auth/oidc/models.go @@ -53,7 +53,7 @@ type AuthRequest struct { CodeChallengeMethod string `gorm:"column:code_challenge_method"` Subject string `gorm:"column:subject"` AuthTime *time.Time `gorm:"column:auth_time"` - Code string `gorm:"column:code"` + Code *string `gorm:"column:code"` IsDone bool `gorm:"column:done;default:false"` CreatedAt time.Time `gorm:"column:created_at"` ExpiresAt time.Time `gorm:"column:expires_at"` diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index fd99f6230..1aad76429 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -5,32 +5,23 @@ import ( "github.com/flanksource/duty/context" "github.com/labstack/echo/v4" - "github.com/tg123/go-htpasswd" ) // MountRoutes sets up OIDC endpoints on the echo server. -// Called from auth.Middleware when OIDC is enabled. -func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath, htpasswdFile string, lookup personLookup) error { +func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath string, checker CredentialChecker, lookup PersonLookup) error { provider, err := NewProvider(ctx, issuerURL, signingKeyPath) if err != nil { return err } - checker, err := htpasswd.New(htpasswdFile, htpasswd.DefaultSystems, nil) - if err != nil { - return err - } - loginHandler := NewLoginHandler(provider.Storage, provider.OpenIDProvider, checker, lookup) e.GET("/oidc/login", loginHandler.ShowForm) e.POST("/oidc/login", loginHandler.HandleSubmit) - // Mount the zitadel OP handler under /oidc (handles authorize, token, userinfo, etc.) oidcHandler := http.StripPrefix("/oidc", provider.Handler) e.Any("/oidc/*", echo.WrapHandler(oidcHandler)) - // Well-known endpoints (discovery, jwks) e.Any("/.well-known/*", echo.WrapHandler(provider.Handler)) return nil diff --git a/auth/oidc_test.go b/auth/oidc_test.go index d798a7dad..512d48447 100644 --- a/auth/oidc_test.go +++ b/auth/oidc_test.go @@ -52,6 +52,8 @@ var _ = ginkgo.Describe("OIDC", func() { ginkgo.AfterEach(func() { DefaultContext.DB().Where("id = ?", person.ID).Delete(&models.Person{}) + DefaultContext.DB().Exec("DELETE FROM oidc_auth_requests") + DefaultContext.DB().Exec("DELETE FROM oidc_refresh_tokens") }) ginkgo.It("creates signing key file on first start", func() { From ab9a21b68065ab0ebb9ffca09d4b3acecb9fbd0d Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 20 Mar 2026 12:29:43 +0200 Subject: [PATCH 04/48] fix(auth): address PR review comments from CodeRabbit and CodeQL - Escape auth_request_id in login form HTML to prevent XSS - Remove duplicate AuthCmd, add login subcommand to existing Auth - Use SHA-256 for AES key derivation instead of raw byte copy - Add 30s timeout to HTTP client in CLI auth login - Use safe type assertion for JWT audience array - Fix "notificatino" typo in jobs error message --- auth/oidc/login.go | 5 +++-- auth/oidc/provider.go | 5 ++--- auth/oidc_validate.go | 2 +- cmd/auth_login.go | 14 +++++--------- jobs/jobs.go | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/auth/oidc/login.go b/auth/oidc/login.go index 1c1258d79..33581e390 100644 --- a/auth/oidc/login.go +++ b/auth/oidc/login.go @@ -2,6 +2,7 @@ package oidc import ( "fmt" + "html" "net/http" "github.com/flanksource/duty/context" @@ -55,7 +56,7 @@ func (h *LoginHandler) ShowForm(c echo.Context) error { if id == "" { return c.String(http.StatusBadRequest, "missing auth_request_id") } - return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, id, "")) + return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, html.EscapeString(id), "")) } func (h *LoginHandler) HandleSubmit(c echo.Context) error { @@ -66,7 +67,7 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { password := c.FormValue("password") renderForm := func(msg string) error { - return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, id, "

"+msg+"

")) + return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, html.EscapeString(id), "

"+msg+"

")) } if id == "" || username == "" || password == "" { diff --git a/auth/oidc/provider.go b/auth/oidc/provider.go index 139620ab4..6a4334f17 100644 --- a/auth/oidc/provider.go +++ b/auth/oidc/provider.go @@ -3,6 +3,7 @@ package oidc import ( "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" @@ -131,7 +132,5 @@ func writeRSAPrivateKey(path string, key *rsa.PrivateKey) error { // aesKeyFromIssuer derives a 32-byte AES key from the issuer URL for OIDC internal encryption. func aesKeyFromIssuer(issuer string) [32]byte { - var key [32]byte - copy(key[:], []byte(issuer)) - return key + return sha256.Sum256([]byte(issuer)) } diff --git a/auth/oidc_validate.go b/auth/oidc_validate.go index 1e69406a8..ec141ab68 100644 --- a/auth/oidc_validate.go +++ b/auth/oidc_validate.go @@ -66,7 +66,7 @@ func authenticateOIDCToken(c echo.Context, tokenStr string) (bool, error) { if auds, ok := claims["aud"].([]any); ok { found := false for _, a := range auds { - if a.(string) == oidcmodels.ClientID { + if audStr, ok := a.(string); ok && audStr == oidcmodels.ClientID { found = true break } diff --git a/cmd/auth_login.go b/cmd/auth_login.go index 5d9ab46a3..c5a828d75 100644 --- a/cmd/auth_login.go +++ b/cmd/auth_login.go @@ -20,11 +20,6 @@ import ( "github.com/spf13/cobra" ) -var AuthCmd = &cobra.Command{ - Use: "auth", - Short: "Authentication commands", -} - var authLoginCmd = &cobra.Command{ Use: "login", Short: "Log in via OIDC browser flow", @@ -36,8 +31,7 @@ var loginServer string func init() { authLoginCmd.Flags().StringVar(&loginServer, "server", "", "Mission Control server URL (required)") _ = authLoginCmd.MarkFlagRequired("server") - AuthCmd.AddCommand(authLoginCmd) - Root.AddCommand(AuthCmd) + Auth.AddCommand(authLoginCmd) } type oidcTokens struct { @@ -161,8 +155,10 @@ type oidcDiscovery struct { UserinfoEndpoint string `json:"userinfo_endpoint"` } +var httpClient = &http.Client{Timeout: 30 * time.Second} + func discoverOIDC(discoveryURL string) (*oidcDiscovery, error) { - resp, err := http.Get(discoveryURL) + resp, err := httpClient.Get(discoveryURL) if err != nil { return nil, err } @@ -201,7 +197,7 @@ func exchangeCode(tokenEndpoint, code, redirectURI, verifier string) (*oidcToken "code_verifier": {verifier}, } - resp, err := http.PostForm(tokenEndpoint, form) + resp, err := httpClient.PostForm(tokenEndpoint, form) if err != nil { return nil, err } diff --git a/jobs/jobs.go b/jobs/jobs.go index 40339c4ed..e8ee4d712 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -111,7 +111,7 @@ func Start(ctx context.Context, mcpServer *server.MCPServer) { } if err := notification.InitCRDStatusUpdates(ctx); err != nil { - logger.Errorf("failed to start notificatino status update queue: %v", err) + logger.Errorf("failed to start notification status update queue: %v", err) } } From 0cc044afa56c4b4478fb5d0a1055185c184e2157 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 20 Mar 2026 22:30:30 +0545 Subject: [PATCH 05/48] chore: pass context to oidc matcher and return err --- auth/basic.go | 8 ++++++-- auth/kratos.go | 6 +++--- auth/oidc/login.go | 6 +++--- auth/oidc_test.go | 7 ++++++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/auth/basic.go b/auth/basic.go index 37a97eb5f..2c65fed60 100644 --- a/auth/basic.go +++ b/auth/basic.go @@ -207,8 +207,12 @@ func NewHtpasswdChecker(path string) (*HtpasswdChecker, error) { return &HtpasswdChecker{file: f}, nil } -func (h *HtpasswdChecker) Match(user, pass string) bool { - return h.file.Match(user, pass) +func (h *HtpasswdChecker) Match(ctx context.Context, user, pass string) error { + match := h.file.Match(user, pass) + if !match { + return fmt.Errorf("invalid credentials") + } + return nil } func lookupPerson(ctx context.Context, user string) (*models.Person, error) { diff --git a/auth/kratos.go b/auth/kratos.go index d8914bf68..b5ec90007 100644 --- a/auth/kratos.go +++ b/auth/kratos.go @@ -241,9 +241,9 @@ func NewKratosCredentialChecker(m *kratosMiddleware) *KratosCredentialChecker { return &KratosCredentialChecker{middleware: m} } -func (k *KratosCredentialChecker) Match(user, pass string) bool { - _, err := k.middleware.kratosLoginWithCache(gocontext.Background(), user, pass) - return err == nil +func (k *KratosCredentialChecker) Match(ctx context.Context, user, pass string) error { + _, err := k.middleware.kratosLoginWithCache(ctx, user, pass) + return err } // LookupKratosPersonByUsername finds a person by email in the Kratos identities table. diff --git a/auth/oidc/login.go b/auth/oidc/login.go index 33581e390..2f6946805 100644 --- a/auth/oidc/login.go +++ b/auth/oidc/login.go @@ -36,7 +36,7 @@ type LoginHandler struct { // CredentialChecker validates username/password credentials. type CredentialChecker interface { - Match(user, pass string) bool + Match(ctx context.Context, user, pass string) error } // PersonLookup finds a person by username/email, returning the person UUID. @@ -74,8 +74,8 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { return renderForm("All fields required") } - if !h.checker.Match(username, password) { - return renderForm("Invalid credentials") + if err := h.checker.Match(ctx, username, password); err != nil { + return renderForm(fmt.Sprintf("Invalid credentials: %v", err)) } personID, err := h.PersonLookup(ctx, username) diff --git a/auth/oidc_test.go b/auth/oidc_test.go index 512d48447..039c455a6 100644 --- a/auth/oidc_test.go +++ b/auth/oidc_test.go @@ -548,7 +548,12 @@ type mockChecker struct { valid bool } -func (m *mockChecker) Match(_, _ string) bool { return m.valid } +func (m *mockChecker) Match(_ dutyContext.Context, _, _ string) error { + if m.valid { + return nil + } + return fmt.Errorf("invalid credentials") +} var mockLookup = func(ctx dutyContext.Context, user string) (string, error) { return uuid.New().String(), nil From be05923057702aef19b4783767539c3e8680d9f0 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 20 Mar 2026 22:32:31 +0545 Subject: [PATCH 06/48] fix(oidc): mark auth request as done after successful login Set done=true in SetAuthRequestSubject when persisting the authenticated subject. Zitadel's /authorize/callback requires AuthRequest.Done() to be true before issuing an authorization code. Without this, the flow fails with interaction_required even after valid credentials. --- auth/oidc/storage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/auth/oidc/storage.go b/auth/oidc/storage.go index 17766afc9..dd7f5567b 100644 --- a/auth/oidc/storage.go +++ b/auth/oidc/storage.go @@ -261,6 +261,7 @@ func (s *Storage) SetAuthRequestSubject(id, subject string) error { Updates(map[string]any{ "subject": subject, "auth_time": now, + "done": true, }).Error } From 5dad17a5e600e5ed46b9cae4ab75cf4f619f583e Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 20 Mar 2026 22:34:17 +0545 Subject: [PATCH 07/48] fix(oidc): prevent nil function panic when restricting token scopes Return identity scope-filter functions for the CLI OIDC client instead of nil from RestrictAdditionalIdTokenScopes and RestrictAdditionalAccessTokenScopes. Zitadel calls these hooks during token creation. Returning nil caused a nil function dereference in pkg/op/token.go (CreateJWT/CreateIDToken), leading to panic during code exchange. --- auth/oidc/models.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/oidc/models.go b/auth/oidc/models.go index 2325a36f4..0aefb5cc8 100644 --- a/auth/oidc/models.go +++ b/auth/oidc/models.go @@ -160,10 +160,10 @@ func (c *cliClient) AccessTokenType() op.AccessTokenType { func (c *cliClient) IDTokenLifetime() time.Duration { return time.Hour } func (c *cliClient) DevMode() bool { return false } func (c *cliClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { - return nil + return func(scopes []string) []string { return scopes } } func (c *cliClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { - return nil + return func(scopes []string) []string { return scopes } } func (c *cliClient) IsScopeAllowed(scope string) bool { return true } func (c *cliClient) IDTokenUserinfoClaimsAssertion() bool { return false } From ceec9e34281d742d5c9042eedb2955881ae2ab72 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 20 Mar 2026 22:42:31 +0545 Subject: [PATCH 08/48] fix(auth): refine OIDC skip-auth path matching --- auth/middleware.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index a3f1a0625..5a3b276ff 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -59,6 +59,20 @@ var skipAuthPathPrefixes = []string{ "/auth/basic/", "/oidc/", "/.well-known/", + "/oauth/", // Standard OIDC protocol endpoints (mounted at root to match the issuer URL). +} + +var skipAuthPathsExact = []string{ + "/health", + + // --start:: Standard OIDC protocol endpoints (mounted at root to match the issuer URL). + "/authorize", + "/userinfo", + "/keys", + "/revoke", + "/device_authorization", + "/end_session", + // --end:: Standard OIDC endpoints } func Middleware(ctx context.Context, e *echo.Echo) error { @@ -146,9 +160,7 @@ func Middleware(ctx context.Context, e *echo.Echo) error { // TODO: Use regex supported path matching func canSkipAuth(c echo.Context) bool { - // use c.Request().URL.Path for exact matches instead of c.Path() which may contain path parameters (e.g. /playbook/webhook/:id) - // Example: URL.PATH = /authorize/callback whereas c.Path() = /authorize/* - if slices.Contains(skipAuthPathsExact, c.Request().URL.Path) { + if slices.Contains(skipAuthPathsExact, c.Path()) { return true } From 6800d87b73073846349c878d0a27b6df5ece3828 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 20 Mar 2026 22:48:43 +0545 Subject: [PATCH 09/48] fix(oidc): expose provider endpoints on discovery paths --- auth/oidc/routes.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index 1aad76429..ee9cadd03 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -1,8 +1,6 @@ package oidc import ( - "net/http" - "github.com/flanksource/duty/context" "github.com/labstack/echo/v4" ) @@ -16,13 +14,22 @@ func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath st loginHandler := NewLoginHandler(provider.Storage, provider.OpenIDProvider, checker, lookup) + // Custom login form (not part of the standard OIDC protocol paths). e.GET("/oidc/login", loginHandler.ShowForm) e.POST("/oidc/login", loginHandler.HandleSubmit) - oidcHandler := http.StripPrefix("/oidc", provider.Handler) - e.Any("/oidc/*", echo.WrapHandler(oidcHandler)) - - e.Any("/.well-known/*", echo.WrapHandler(provider.Handler)) + // Standard OIDC protocol endpoints — mounted at the root so that the issuer URL + // and the authorization_endpoint/token_endpoint values in the discovery document + // resolve to real routes on this server. + h := echo.WrapHandler(provider.Handler) + e.Any("/authorize", h) + e.Any("/authorize/*", h) + e.Any("/oauth/token", h) + e.Any("/oauth/introspect", h) + e.Any("/userinfo", h) + e.Any("/keys", h) + e.Any("/endsession", h) + e.Any("/.well-known/*", h) return nil } From 17c83554d7fae40fb33bec2d99513b28ab305db3 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Fri, 20 Mar 2026 23:43:57 +0545 Subject: [PATCH 10/48] fix: support /authorize prefix due to /authorize/callback --- auth/middleware.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/auth/middleware.go b/auth/middleware.go index 5a3b276ff..89d4be27b 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -67,6 +67,7 @@ var skipAuthPathsExact = []string{ // --start:: Standard OIDC protocol endpoints (mounted at root to match the issuer URL). "/authorize", + "/authorize/callback", "/userinfo", "/keys", "/revoke", @@ -160,7 +161,9 @@ func Middleware(ctx context.Context, e *echo.Echo) error { // TODO: Use regex supported path matching func canSkipAuth(c echo.Context) bool { - if slices.Contains(skipAuthPathsExact, c.Path()) { + // use c.Request().URL.Path for exact matches instead of c.Path() which may contain path parameters (e.g. /playbook/webhook/:id) + // Example: URL.PATH = /authorize/callback whereas c.Path() = /authorize/* + if slices.Contains(skipAuthPathsExact, c.Request().URL.Path) { return true } From d7ad4b3fc870aadf9f08f0a75292ec6247822245 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 22 Mar 2026 13:29:14 +0200 Subject: [PATCH 11/48] feat(auth): add embedded oidc provider with styled login ui and token hashing Implements a complete OIDC provider for CLI authentication with token security enhancements. Introduces cryptographically secure token storage using SHA-256 hashing for short-lived auth codes and refresh tokens, and separate AES-256 key generation for OIDC state encryption. Adds professionally styled login and callback success pages with Tailwind CSS. Includes CLI tools for PKCE-based OAuth flows and comprehensive security documentation. --- Makefile | 11 +- auth/oidc/login.go | 32 ++---- auth/oidc/provider.go | 45 +++++++- auth/oidc/routes.go | 9 +- auth/oidc/storage.go | 41 +++++--- auth/oidc_test.go | 19 ++-- auth/oidc_validate.go | 2 +- auth/oidcclient/oidcclient.go | 18 +--- cmd/auth_login.go | 157 +++++----------------------- go.mod | 7 +- tests/oidc_e2e/oidc-signing-key.pem | 27 +++++ tests/oidc_e2e/oidc_login_test.go | 142 +++++++++++++++++++++++++ tests/oidc_e2e/suite_test.go | 110 +++++++++++++++++++ 13 files changed, 410 insertions(+), 210 deletions(-) create mode 100644 tests/oidc_e2e/oidc-signing-key.pem create mode 100644 tests/oidc_e2e/oidc_login_test.go create mode 100644 tests/oidc_e2e/suite_test.go diff --git a/Makefile b/Makefile index e5f809fa6..c23d9f91d 100644 --- a/Makefile +++ b/Makefile @@ -144,12 +144,21 @@ manifests: generate gen-schemas ## Generate WebhookConfiguration, ClusterRole an generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object paths="./api/..." paths="./logs/..." +TAILWIND_VERSION ?= 3.4.17 +TAILWIND_JS = auth/oidc/static/tailwind.min.js + +$(TAILWIND_JS): + curl -sL "https://cdn.tailwindcss.com/$(TAILWIND_VERSION)" -o $(TAILWIND_JS) + +.PHONY: static +static: $(TAILWIND_JS) + .PHONY: build build: static go build -o ./.bin/$(NAME) -ldflags "-X \"main.version=$(VERSION_TAG) built at $(DATE)\"" main.go .PHONY: dev -dev: +dev: static # Disabling CGO because of slow build times in apple silicon (just experimenting) CGO_ENABLED=0 go build -v -o ./.bin/$(NAME) -gcflags="all=-N -l" main.go diff --git a/auth/oidc/login.go b/auth/oidc/login.go index 2f6946805..ac38333b9 100644 --- a/auth/oidc/login.go +++ b/auth/oidc/login.go @@ -6,48 +6,32 @@ import ( "net/http" "github.com/flanksource/duty/context" + "github.com/flanksource/incident-commander/auth/oidc/static" "github.com/labstack/echo/v4" "github.com/zitadel/oidc/v3/pkg/op" ) -const loginFormHTML = ` - -Mission Control Login - -

Sign in

-
- -
-
- -
-%s - -` - -// LoginHandler handles the OIDC login form, delegating credential validation -// to the Basic auth checker and person lookup. type LoginHandler struct { storage *Storage provider op.OpenIDProvider checker CredentialChecker PersonLookup PersonLookup + issuerURL string } -// CredentialChecker validates username/password credentials. type CredentialChecker interface { Match(ctx context.Context, user, pass string) error } -// PersonLookup finds a person by username/email, returning the person UUID. type PersonLookup func(ctx context.Context, user string) (personID string, err error) -func NewLoginHandler(storage *Storage, provider op.OpenIDProvider, checker CredentialChecker, lookup PersonLookup) *LoginHandler { +func NewLoginHandler(storage *Storage, provider op.OpenIDProvider, checker CredentialChecker, lookup PersonLookup, issuerURL string) *LoginHandler { return &LoginHandler{ storage: storage, provider: provider, checker: checker, PersonLookup: lookup, + issuerURL: issuerURL, } } @@ -56,7 +40,7 @@ func (h *LoginHandler) ShowForm(c echo.Context) error { if id == "" { return c.String(http.StatusBadRequest, "missing auth_request_id") } - return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, html.EscapeString(id), "")) + return c.HTML(http.StatusOK, fmt.Sprintf(static.LoginHTML, html.EscapeString(id), "")) } func (h *LoginHandler) HandleSubmit(c echo.Context) error { @@ -67,7 +51,8 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { password := c.FormValue("password") renderForm := func(msg string) error { - return c.HTML(http.StatusOK, fmt.Sprintf(loginFormHTML, html.EscapeString(id), "

"+msg+"

")) + return c.HTML(http.StatusOK, fmt.Sprintf(static.LoginHTML, html.EscapeString(id), + `

`+html.EscapeString(msg)+`

`)) } if id == "" || username == "" || password == "" { @@ -87,6 +72,7 @@ func (h *LoginHandler) HandleSubmit(c echo.Context) error { return renderForm("Internal error") } - callbackURL := op.AuthCallbackURL(h.provider)(c.Request().Context(), id) + issuerCtx := op.ContextWithIssuer(c.Request().Context(), h.issuerURL) + callbackURL := op.AuthCallbackURL(h.provider)(issuerCtx, id) return c.Redirect(http.StatusFound, callbackURL) } diff --git a/auth/oidc/provider.go b/auth/oidc/provider.go index 6a4334f17..2c659a21e 100644 --- a/auth/oidc/provider.go +++ b/auth/oidc/provider.go @@ -3,7 +3,6 @@ package oidc import ( "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" @@ -32,6 +31,11 @@ func NewProvider(ctx context.Context, issuerURL, signingKeyPath string) (*Provid return nil, fmt.Errorf("oidc signing key: %w", err) } + cryptoKey, err := loadOrGenerateCryptoKey(signingKeyPath) + if err != nil { + return nil, fmt.Errorf("oidc crypto key: %w", err) + } + signer := &signingKey{ id: keyID, algorithm: "RS256", @@ -40,7 +44,8 @@ func NewProvider(ctx context.Context, issuerURL, signingKeyPath string) (*Provid storage := NewStorage(ctx, signer) config := &op.Config{ - CryptoKey: aesKeyFromIssuer(issuerURL), + CryptoKey: cryptoKey, + GrantTypeRefreshToken: true, } oidcProvider, err := op.NewProvider(config, storage, @@ -130,7 +135,37 @@ func writeRSAPrivateKey(path string, key *rsa.PrivateKey) error { return os.WriteFile(path, data, 0600) } -// aesKeyFromIssuer derives a 32-byte AES key from the issuer URL for OIDC internal encryption. -func aesKeyFromIssuer(issuer string) [32]byte { - return sha256.Sum256([]byte(issuer)) +func loadOrGenerateCryptoKey(keyPath string) ([32]byte, error) { + if keyPath == "" { + keyPath = "oidc-crypto-key.key" + } else { + keyPath = keyPath + ".crypto" + } + + var key [32]byte + data, err := os.ReadFile(keyPath) + if err == nil { + if len(data) == 32 { + copy(key[:], data) + logger.Infof("OIDC: loaded crypto key from %s", keyPath) + return key, nil + } + return key, fmt.Errorf("crypto key at %s has invalid length %d (expected 32)", keyPath, len(data)) + } + + if !os.IsNotExist(err) { + return key, fmt.Errorf("read crypto key file %s: %w", keyPath, err) + } + + // Generate new key + if _, err := rand.Read(key[:]); err != nil { + return key, fmt.Errorf("generate crypto key: %w", err) + } + + if err := os.WriteFile(keyPath, key[:], 0600); err != nil { + return key, fmt.Errorf("write crypto key: %w", err) + } + logger.Infof("OIDC: generated new crypto key at %s", keyPath) + + return key, nil } diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index ee9cadd03..00a010402 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -2,17 +2,22 @@ package oidc import ( "github.com/flanksource/duty/context" + "github.com/flanksource/incident-commander/auth/oidc/static" "github.com/labstack/echo/v4" ) // MountRoutes sets up OIDC endpoints on the echo server. +// The OIDC provider is mounted under /oidc/ with the issuer set to {issuerURL}/oidc +// so that discovery at /oidc/.well-known/openid-configuration returns correct endpoint URLs. +// A convenience redirect from /.well-known/openid-configuration is provided for standard discovery. func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath string, checker CredentialChecker, lookup PersonLookup) error { - provider, err := NewProvider(ctx, issuerURL, signingKeyPath) + oidcIssuer := strings.TrimRight(issuerURL, "/") + "/oidc" + provider, err := NewProvider(ctx, oidcIssuer, signingKeyPath) if err != nil { return err } - loginHandler := NewLoginHandler(provider.Storage, provider.OpenIDProvider, checker, lookup) + loginHandler := NewLoginHandler(provider.Storage, provider.OpenIDProvider, checker, lookup, oidcIssuer) // Custom login form (not part of the standard OIDC protocol paths). e.GET("/oidc/login", loginHandler.ShowForm) diff --git a/auth/oidc/storage.go b/auth/oidc/storage.go index dd7f5567b..476fefbf3 100644 --- a/auth/oidc/storage.go +++ b/auth/oidc/storage.go @@ -4,7 +4,9 @@ import ( gocontext "context" "crypto" "crypto/rsa" + "crypto/sha256" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" "fmt" @@ -84,7 +86,7 @@ func (s *Storage) AuthRequestByID(_ gocontext.Context, id string) (op.AuthReques func (s *Storage) AuthRequestByCode(_ gocontext.Context, code string) (op.AuthRequest, error) { var ar AuthRequest - if err := s.ctx.DB().Where("code = ? AND expires_at > NOW()", code).First(&ar).Error; err != nil { + if err := s.ctx.DB().Where("code = ? AND expires_at > NOW()", hashToken(code)).First(&ar).Error; err != nil { return nil, fmt.Errorf("auth request not found: %w", err) } return &ar, nil @@ -92,7 +94,7 @@ func (s *Storage) AuthRequestByCode(_ gocontext.Context, code string) (op.AuthRe func (s *Storage) SaveAuthCode(_ gocontext.Context, id, code string) error { return s.ctx.DB().Model(&AuthRequest{}).Where("id = ?", id). - Updates(map[string]any{"code": code, "done": true}).Error + Updates(map[string]any{"code": hashToken(code), "done": true}).Error } func (s *Storage) DeleteAuthRequest(_ gocontext.Context, id string) error { @@ -112,26 +114,28 @@ func (s *Storage) CreateAccessAndRefreshTokens(_ gocontext.Context, req op.Token if currentRefreshToken != "" { // find existing rotation family var existing RefreshToken - if err := s.ctx.DB().Where("token = ?", currentRefreshToken).First(&existing).Error; err == nil { + if err := s.ctx.DB().Where("token = ?", hashToken(currentRefreshToken)).First(&existing).Error; err == nil { rotationID = existing.RotationID // rotate: mark old token expired - s.ctx.DB().Model(&RefreshToken{}).Where("token = ?", currentRefreshToken). + s.ctx.DB().Model(&RefreshToken{}).Where("token = ?", hashToken(currentRefreshToken)). Update("expires_at", time.Now()) } } - ar, ok := req.(*AuthRequest) - if !ok { - return "", "", time.Time{}, fmt.Errorf("unexpected request type %T", req) + now := time.Now() + clientID := ClientID + if aud := req.GetAudience(); len(aud) > 0 { + clientID = aud[0] } - now := time.Now() + rawRefreshToken := uuid.New().String() + rt := &RefreshToken{ ID: uuid.New().String(), - Token: uuid.New().String(), - ClientID: ar.ClientID, - Subject: ar.Subject, - Scopes: ar.Scopes, + Token: hashToken(rawRefreshToken), + ClientID: clientID, + Subject: req.GetSubject(), + Scopes: StringList(req.GetScopes()), AuthTime: now, RotationID: rotationID, CreatedAt: now, @@ -141,12 +145,12 @@ func (s *Storage) CreateAccessAndRefreshTokens(_ gocontext.Context, req op.Token return "", "", time.Time{}, fmt.Errorf("create refresh token: %w", err) } - return accessTokenID, rt.Token, expiry, nil + return accessTokenID, rawRefreshToken, expiry, nil } func (s *Storage) TokenRequestByRefreshToken(_ gocontext.Context, refreshToken string) (op.RefreshTokenRequest, error) { var rt RefreshToken - if err := s.ctx.DB().Where("token = ? AND expires_at > NOW()", refreshToken).First(&rt).Error; err != nil { + if err := s.ctx.DB().Where("token = ? AND expires_at > NOW()", hashToken(refreshToken)).First(&rt).Error; err != nil { return nil, op.ErrInvalidRefreshToken } return &rt, nil @@ -161,7 +165,7 @@ func (s *Storage) RevokeToken(_ gocontext.Context, tokenOrID, userID, _ string) if userID != "" { query = s.ctx.DB().Where("id = ? AND subject = ?", tokenOrID, userID) } else { - query = s.ctx.DB().Where("token = ?", tokenOrID) + query = s.ctx.DB().Where("id = ? OR token = ?", tokenOrID, hashToken(tokenOrID)) } if err := query.Delete(&RefreshToken{}).Error; err != nil { return oidc.ErrServerError() @@ -171,7 +175,7 @@ func (s *Storage) RevokeToken(_ gocontext.Context, tokenOrID, userID, _ string) func (s *Storage) GetRefreshTokenInfo(_ gocontext.Context, _, token string) (string, string, error) { var rt RefreshToken - if err := s.ctx.DB().Where("token = ?", token).First(&rt).Error; err != nil { + if err := s.ctx.DB().Where("token = ?", hashToken(token)).First(&rt).Error; err != nil { return "", "", op.ErrInvalidRefreshToken } return rt.Subject, rt.ID, nil @@ -300,3 +304,8 @@ func generateKeyID(pub *rsa.PublicKey) (string, error) { h.Write(b) return fmt.Sprintf("%x", h.Sum(nil))[:16], nil } + +func hashToken(token string) string { + h := sha256.Sum256([]byte(token)) + return base64.RawURLEncoding.EncodeToString(h[:]) +} diff --git a/auth/oidc_test.go b/auth/oidc_test.go index 039c455a6..ba269712f 100644 --- a/auth/oidc_test.go +++ b/auth/oidc_test.go @@ -270,13 +270,12 @@ var _ = ginkgo.Describe("OIDC", func() { // Clear the cache so it reloads from DB oidcPublicKeyCache.Flush() - issuer := "http://localhost:8080" savedPublicURL := api.PublicURL - api.PublicURL = issuer + api.PublicURL = "http://localhost:8080" defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": issuer, + "iss": "http://localhost:8080/oidc", "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -302,7 +301,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080", + "iss": "http://localhost:8080/oidc", "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -329,7 +328,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080", + "iss": "http://localhost:8080/oidc", "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(-1 * time.Hour).Unix(), @@ -356,7 +355,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080", + "iss": "http://localhost:8080/oidc", "aud": "wrong-client", "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -383,7 +382,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080", + "iss": "http://localhost:8080/oidc", "aud": oidc.ClientID, "sub": uuid.New().String(), // non-existent person "exp": time.Now().Add(time.Hour).Unix(), @@ -401,7 +400,7 @@ var _ = ginkgo.Describe("OIDC", func() { ginkgo.Describe("LoginHandler", func() { ginkgo.It("renders login form with auth_request_id", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup) + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080/oidc") e := newEchoInstance(DefaultContext) req := httptest.NewRequest(http.MethodGet, "/oidc/login?auth_request_id=test-123", nil) req = req.WithContext(DefaultContext.Wrap(req.Context())) @@ -415,7 +414,7 @@ var _ = ginkgo.Describe("OIDC", func() { }) ginkgo.It("returns 400 when auth_request_id is missing", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup) + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080/oidc") e := newEchoInstance(DefaultContext) req := httptest.NewRequest(http.MethodGet, "/oidc/login", nil) req = req.WithContext(DefaultContext.Wrap(req.Context())) @@ -427,7 +426,7 @@ var _ = ginkgo.Describe("OIDC", func() { }) ginkgo.It("rejects invalid credentials", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{valid: false}, mockLookup) + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{valid: false}, mockLookup, "http://localhost:8080/oidc") e := newEchoInstance(DefaultContext) form := url.Values{ diff --git a/auth/oidc_validate.go b/auth/oidc_validate.go index ec141ab68..48b441785 100644 --- a/auth/oidc_validate.go +++ b/auth/oidc_validate.go @@ -35,7 +35,7 @@ func authenticateOIDCToken(c echo.Context, tokenStr string) (bool, error) { return false, nil } - issuer := api.PublicURL + issuer := strings.TrimRight(api.PublicURL, "/") + "/oidc" var lastErr error for _, pub := range keys { diff --git a/auth/oidcclient/oidcclient.go b/auth/oidcclient/oidcclient.go index 733c7ac13..cb9b211eb 100644 --- a/auth/oidcclient/oidcclient.go +++ b/auth/oidcclient/oidcclient.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io" "net/http" "net/url" "strings" @@ -35,15 +34,6 @@ func Discover(discoveryURL string) (*Discovery, error) { } defer resp.Body.Close() - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { - body, _ := io.ReadAll(io.LimitReader(resp.Body, 256)) - msg := strings.TrimSpace(string(body)) - if msg == "" { - return nil, fmt.Errorf("discovery endpoint returned %s", resp.Status) - } - return nil, fmt.Errorf("discovery endpoint returned %s: %s", resp.Status, msg) - } - var d Discovery if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { return nil, err @@ -62,12 +52,10 @@ func GeneratePKCE() (verifier, challenge string, err error) { return } -func RandomBase64(n int) (string, error) { +func RandomBase64(n int) string { b := make([]byte, n) - if _, err := rand.Read(b); err != nil { - return "", err - } - return base64.RawURLEncoding.EncodeToString(b), nil + _, _ = rand.Read(b) + return base64.RawURLEncoding.EncodeToString(b) } func ExchangeCode(tokenEndpoint, code, redirectURI, verifier string) (*Tokens, error) { diff --git a/cmd/auth_login.go b/cmd/auth_login.go index c5a828d75..ad4959679 100644 --- a/cmd/auth_login.go +++ b/cmd/auth_login.go @@ -2,9 +2,6 @@ package cmd import ( "context" - "crypto/rand" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "net" @@ -17,6 +14,8 @@ import ( "strings" "time" + "github.com/flanksource/incident-commander/auth/oidc/static" + "github.com/flanksource/incident-commander/auth/oidcclient" "github.com/spf13/cobra" ) @@ -34,38 +33,30 @@ func init() { Auth.AddCommand(authLoginCmd) } -type oidcTokens struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - IDToken string `json:"id_token"` - ExpiresAt time.Time `json:"expires_at"` -} - func runAuthLogin(cmd *cobra.Command, _ []string) error { serverURL := strings.TrimRight(loginServer, "/") - // Discover OIDC endpoints - discoveryURL := serverURL + "/.well-known/openid-configuration" - endpoints, err := discoverOIDC(discoveryURL) + endpoints, err := oidcclient.Discover(serverURL + "/.well-known/openid-configuration") if err != nil { return fmt.Errorf("OIDC discovery failed: %w", err) } - // Generate PKCE values - verifier, challenge, err := generatePKCE() + verifier, challenge, err := oidcclient.GeneratePKCE() if err != nil { return fmt.Errorf("PKCE generation failed: %w", err) } - state := randomBase64(16) - nonce := randomBase64(16) + state := oidcclient.RandomBase64(16) + nonce := oidcclient.RandomBase64(16) - // Start local callback server listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return fmt.Errorf("failed to start local server: %w", err) } redirectURI := fmt.Sprintf("http://127.0.0.1:%d/callback", listener.Addr().(*net.TCPAddr).Port) + // Render the success page with absolute URLs to the MC server's static assets + successHTML := strings.ReplaceAll(static.CallbackSuccessHTML, "/oidc/static/", serverURL+"/oidc/static/") + codeCh := make(chan string, 1) errCh := make(chan error, 1) server := &http.Server{} @@ -92,7 +83,8 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { errCh <- fmt.Errorf("missing authorization code") return } - fmt.Fprintf(w, "

Login successful!

You can close this tab.

") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, successHTML) codeCh <- code }) @@ -103,7 +95,6 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { }() defer func() { _ = server.Shutdown(context.Background()) }() - // Build authorize URL authURL := fmt.Sprintf("%s?client_id=mc-cli&response_type=code&scope=%s&redirect_uri=%s&state=%s&nonce=%s&code_challenge=%s&code_challenge_method=S256", endpoints.AuthorizationEndpoint, url.QueryEscape("openid profile email offline_access"), @@ -116,7 +107,6 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { fmt.Fprintf(cmd.OutOrStdout(), "Opening browser for login...\n%s\n\n", authURL) openBrowser(authURL) - // Wait for callback var code string select { case code = <-codeCh: @@ -126,145 +116,46 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { return fmt.Errorf("login timed out") } - // Exchange code for tokens - tokens, err := exchangeCode(endpoints.TokenEndpoint, code, redirectURI, verifier) + tokens, err := oidcclient.ExchangeCode(endpoints.TokenEndpoint, code, redirectURI, verifier) if err != nil { return fmt.Errorf("token exchange failed: %w", err) } - // Validate nonce in ID token - if err := validateNonce(tokens.IDToken, nonce); err != nil { + if err := oidcclient.ValidateNonce(tokens.IDToken, nonce); err != nil { return fmt.Errorf("nonce validation failed: %w", err) } - // Store tokens - if err := storeTokens(serverURL, tokens); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "warning: could not save tokens: %v\n", err) - } - - fmt.Fprintf(cmd.OutOrStdout(), "\nLogin successful!\n\n") - fmt.Fprintf(cmd.OutOrStdout(), "Access token:\n%s\n\n", tokens.AccessToken) - fmt.Fprintf(cmd.OutOrStdout(), "Usage example:\n curl -H \"Authorization: Bearer %s\" %s/auth/whoami\n", tokens.AccessToken, serverURL) - - return nil -} - -type oidcDiscovery struct { - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - UserinfoEndpoint string `json:"userinfo_endpoint"` -} - -var httpClient = &http.Client{Timeout: 30 * time.Second} - -func discoverOIDC(discoveryURL string) (*oidcDiscovery, error) { - resp, err := httpClient.Get(discoveryURL) + tokenPath, err := storeTokens(serverURL, tokens) if err != nil { - return nil, err - } - defer resp.Body.Close() - - var d oidcDiscovery - if err := json.NewDecoder(resp.Body).Decode(&d); err != nil { - return nil, err - } - return &d, nil -} - -func generatePKCE() (verifier, challenge string, err error) { - b := make([]byte, 32) - if _, err = rand.Read(b); err != nil { - return + return fmt.Errorf("failed to save tokens: %w", err) } - verifier = base64.RawURLEncoding.EncodeToString(b) - h := sha256.Sum256([]byte(verifier)) - challenge = base64.RawURLEncoding.EncodeToString(h[:]) - return -} - -func randomBase64(n int) string { - b := make([]byte, n) - _, _ = rand.Read(b) - return base64.RawURLEncoding.EncodeToString(b) -} -func exchangeCode(tokenEndpoint, code, redirectURI, verifier string) (*oidcTokens, error) { - form := url.Values{ - "grant_type": {"authorization_code"}, - "code": {code}, - "redirect_uri": {redirectURI}, - "client_id": {"mc-cli"}, - "code_verifier": {verifier}, - } - - resp, err := httpClient.PostForm(tokenEndpoint, form) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("token endpoint returned %d", resp.StatusCode) - } - - var result struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - IDToken string `json:"id_token"` - ExpiresIn int `json:"expires_in"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err - } - - return &oidcTokens{ - AccessToken: result.AccessToken, - RefreshToken: result.RefreshToken, - IDToken: result.IDToken, - ExpiresAt: time.Now().Add(time.Duration(result.ExpiresIn) * time.Second), - }, nil -} + fmt.Fprintf(cmd.OutOrStdout(), "\nLogin successful!\n\n") + fmt.Fprintf(cmd.OutOrStdout(), "Tokens saved to: %s\n\n", tokenPath) + fmt.Fprintf(cmd.OutOrStdout(), "Access token (expires %s):\n%s\n\n", tokens.ExpiresAt.Format("15:04:05"), tokens.AccessToken) + fmt.Fprintf(cmd.OutOrStdout(), "Refresh token:\n%s\n\n", tokens.RefreshToken) -// validateNonce extracts the nonce claim from an ID token (without full verification). -func validateNonce(idToken, expectedNonce string) error { - parts := strings.Split(idToken, ".") - if len(parts) != 3 { - return fmt.Errorf("invalid ID token format") - } - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return fmt.Errorf("decode ID token payload: %w", err) - } - var claims map[string]any - if err := json.Unmarshal(payload, &claims); err != nil { - return fmt.Errorf("parse ID token claims: %w", err) - } - nonce, _ := claims["nonce"].(string) - if nonce != expectedNonce { - return fmt.Errorf("nonce mismatch") - } return nil } -func storeTokens(serverURL string, tokens *oidcTokens) error { +func storeTokens(serverURL string, tokens *oidcclient.Tokens) (string, error) { dir, err := os.UserConfigDir() if err != nil { - return err + return "", err } dir = filepath.Join(dir, "mission-control") if err := os.MkdirAll(dir, 0700); err != nil { - return err + return "", err } - // Store per server host := strings.NewReplacer("://", "_", "/", "_", ":", "_").Replace(serverURL) path := filepath.Join(dir, fmt.Sprintf("tokens_%s.json", host)) data, err := json.MarshalIndent(tokens, "", " ") if err != nil { - return err + return "", err } - return os.WriteFile(path, data, 0600) + return path, os.WriteFile(path, data, 0600) } func openBrowser(url string) { diff --git a/go.mod b/go.mod index b4f174045..fde809c3a 100644 --- a/go.mod +++ b/go.mod @@ -43,8 +43,6 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.11 github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0 github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 - github.com/charmbracelet/huh v1.0.0 - github.com/chromedp/cdproto v0.0.0-20260320225252-cf654f46fc63 github.com/chromedp/chromedp v0.15.0 github.com/emersion/go-message v0.18.2 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 @@ -167,6 +165,7 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/chromedp/cdproto v0.0.0-20260320225252-cf654f46fc63 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -440,8 +439,8 @@ require ( golang.org/x/crypto v0.49.0 golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 golang.org/x/time v0.14.0 // indirect google.golang.org/api v0.262.0 google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed // indirect diff --git a/tests/oidc_e2e/oidc-signing-key.pem b/tests/oidc_e2e/oidc-signing-key.pem new file mode 100644 index 000000000..95608dd8b --- /dev/null +++ b/tests/oidc_e2e/oidc-signing-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAw/iXq6C+/jPwgtjaIOM2MJ50LTlM/yJgPQFn7na4MsVZ0Erj +h3AottVS7A/69BZWBvxpm6C2PGnyCv9er6DQ7t5/wB2PQ5zw9TvlNlgefPHrmOlM +oZHV7sLmA1vnRNLqBIs9LIxAJeXVR3lLG6yRlLeXuYgqgnZfQpNahcdcwWB+hAJZ +83d1lcFSUwCEa1rR7CumfrJHL6bGAk2t7aMIqIPu2PHn5hU2msXgTXwCHavFXU8U +vMTYCTHkA+eG6WStkw0YpXR7m7Q2GC9PWiMQbZJAs3QoGXsT6MpPbps45YJrpHrB +zZPrjOOb1laPjQKY6dtHnd91Fm1rDRi9K97WmwIDAQABAoIBAAT5o65/9zlW5W08 ++iF80ALHkwviErolC986N091HiBtccx5VwtrzCADWTnHy63OTCk2cUWGyZM0RI8F +t3t39rXwJu6GqzA8cY8x3q2hhEY7XtxtiwkZcOsncYipfzz4Q2hE7Kzb2dxC2HmA +pjWx2aq2R0wDg/WQWCmSU7Yy8hmxDdktoP7WZWdzpXWhLLasuUn9EatnHCk38KIW +X+l6dwzhBwW23fc56m/x2LrDMZrYGaXjGMdgIZ1meUADZ1D5vSArqcniS4QWnUco +S8iDOvJ8YEArtTAkUrLAKVOSJhQL0UBemd5JcMN7LtxcOU38gp4GpRs05gEz5ggP +J3yGWjkCgYEA7xFLNV9Mr+j61uWNWW0zdBNtnFBnhJ/fvnohbVl/bLRx3vl5c374 +/G/GbAvG/I1l719utcs2WE3M/3fD9zkUlbeuXKYRoMO5/6d22nNI/v4hcS0bi3ll +eEq0dlB+fi31ac8JmetT8P0tFU5d7WILQdBp0MBP7nbNvaFiBgnJH10CgYEA0dni +mSrNRxFVX9/EHAeQhfUPVTmWvnL1XZGec2Bp0C8E/6ogle5QYKfUqxe2wALLIUuG +aVVtui4p3M9emDdOI5CIMRjCzgtwwVKozWdD4xLLkd0xmRwNK4Aac8MTB0ADl0zF +d8G8qbPBnNmpC3DBw5Vy1Ck7BYvaBIO/5OFdBlcCgYEApC19N88VrCzUruYAV2ye +DFYXTWUOYk8k6fuXny/6SV32YYl0NbP6K+pbGvJPmjtEyMoCDsjarnPnl33ZT5uc +nmEBVlEaBAzGXGLWRZkshSljMAUpSHR7EcxD+Ii5BdBsHFj5oAGzqOlFn78s1Awj +7PPC54BapEpkapk85yarP2UCgYEApC/hhw1iBxScw6KEAZo5jVWrZXblZqTzLP5e +Bs+MeoIPWyl0zVnkMXuefS7UoW6OF8LLq2ZXr6+muUjWLio3TMQsip1g7W0uD1Pp +FqyRBP0ToVB7GdxEwaA9Eg2yuZ1wMdSzO3utbOljtqDmDjluoOQxL6YCs9g0AAxE ++4MIEYkCgYAXkbBV+vU5DgwCS9ENVZX4zM31IzfuSf/iAFuA1Oav05kXQ8XXE3S/ +D+iiEL7+sodp3bdg/yDfVTpy4YpRupx09H06ZDAkc+YuZY0Zn7cKIDTxX2AqFvui +NRfFSnuA0HqAjs85LHKZzXdEvS0wrvLVabVKcD9m3RDmW4I9qQIWuQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/oidc_e2e/oidc_login_test.go b/tests/oidc_e2e/oidc_login_test.go new file mode 100644 index 000000000..eee444949 --- /dev/null +++ b/tests/oidc_e2e/oidc_login_test.go @@ -0,0 +1,142 @@ +package oidc_e2e + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/chromedp/chromedp" + "github.com/flanksource/incident-commander/auth/oidcclient" + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe("OIDC Browser Login Flow", ginkgo.Label("slow"), ginkgo.Ordered, func() { + var tokens *oidcclient.Tokens + var endpoints *oidcclient.Discovery + + ginkgo.It("completes full OIDC authorization code flow via browser", func() { + verifier, challenge, err := oidcclient.GeneratePKCE() + Expect(err).ToNot(HaveOccurred()) + + state := oidcclient.RandomBase64(16) + nonce := oidcclient.RandomBase64(16) + + endpoints, err = oidcclient.Discover(serverURL + "/.well-known/openid-configuration") + Expect(err).ToNot(HaveOccurred()) + Expect(endpoints.AuthorizationEndpoint).ToNot(BeEmpty()) + Expect(endpoints.TokenEndpoint).ToNot(BeEmpty()) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + Expect(err).ToNot(HaveOccurred()) + callbackPort := listener.Addr().(*net.TCPAddr).Port + redirectURI := fmt.Sprintf("http://127.0.0.1:%d/callback", callbackPort) + + codeCh := make(chan string, 1) + errCh := make(chan error, 1) + callbackServer := &http.Server{} + callbackServer.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/callback" { + http.NotFound(w, r) + return + } + q := r.URL.Query() + if s := q.Get("state"); s != state { + errCh <- fmt.Errorf("state mismatch: got %s", s) + http.Error(w, "state mismatch", http.StatusBadRequest) + return + } + if e := q.Get("error"); e != "" { + errCh <- fmt.Errorf("auth error: %s: %s", e, q.Get("error_description")) + http.Error(w, e, http.StatusBadRequest) + return + } + code := q.Get("code") + if code == "" { + errCh <- fmt.Errorf("missing code") + http.Error(w, "missing code", http.StatusBadRequest) + return + } + fmt.Fprint(w, "Login successful") + codeCh <- code + }) + go func() { + if err := callbackServer.Serve(listener); err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() + defer func() { _ = callbackServer.Shutdown(context.Background()) }() + + authURL := fmt.Sprintf("%s?client_id=mc-cli&response_type=code&scope=%s&redirect_uri=%s&state=%s&nonce=%s&code_challenge=%s&code_challenge_method=S256", + endpoints.AuthorizationEndpoint, + url.QueryEscape("openid profile email offline_access"), + url.QueryEscape(redirectURI), + url.QueryEscape(state), + url.QueryEscape(nonce), + url.QueryEscape(challenge), + ) + + err = chromedp.Run(chromectx, + chromedp.Navigate(authURL), + chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery), + chromedp.SendKeys(`input[name="username"]`, "admin", chromedp.ByQuery), + chromedp.SendKeys(`input[name="password"]`, "admin", chromedp.ByQuery), + chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), + ) + Expect(err).ToNot(HaveOccurred()) + + var code string + select { + case code = <-codeCh: + case err := <-errCh: + ginkgo.Fail(fmt.Sprintf("callback error: %v", err)) + case <-time.After(30 * time.Second): + ginkgo.Fail("timed out waiting for callback") + } + Expect(code).ToNot(BeEmpty()) + + tokens, err = oidcclient.ExchangeCode(endpoints.TokenEndpoint, code, redirectURI, verifier) + Expect(err).ToNot(HaveOccurred()) + Expect(tokens.AccessToken).ToNot(BeEmpty()) + Expect(tokens.IDToken).ToNot(BeEmpty()) + Expect(tokens.RefreshToken).ToNot(BeEmpty()) + + Expect(oidcclient.ValidateNonce(tokens.IDToken, nonce)).To(Succeed()) + + req, err := http.NewRequest("GET", serverURL+"/oidc/userinfo", nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Set("Authorization", "Bearer "+tokens.AccessToken) + + resp, err := http.DefaultClient.Do(req) + Expect(err).ToNot(HaveOccurred()) + resp.Body.Close() + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }) + + ginkgo.It("refreshes tokens using the refresh token", func() { + Expect(tokens).ToNot(BeNil(), "login test must run first") + Expect(endpoints).ToNot(BeNil()) + + originalAccess := tokens.AccessToken + originalRefresh := tokens.RefreshToken + + refreshed, err := oidcclient.RefreshToken(endpoints.TokenEndpoint, originalRefresh) + Expect(err).ToNot(HaveOccurred()) + Expect(refreshed.AccessToken).ToNot(BeEmpty()) + Expect(refreshed.RefreshToken).ToNot(BeEmpty()) + Expect(refreshed.AccessToken).ToNot(Equal(originalAccess)) + + // Verify new access token works + req, err := http.NewRequest("GET", serverURL+"/oidc/userinfo", nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Set("Authorization", "Bearer "+refreshed.AccessToken) + + resp, err := http.DefaultClient.Do(req) + Expect(err).ToNot(HaveOccurred()) + resp.Body.Close() + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }) +}) diff --git a/tests/oidc_e2e/suite_test.go b/tests/oidc_e2e/suite_test.go new file mode 100644 index 000000000..cf2959953 --- /dev/null +++ b/tests/oidc_e2e/suite_test.go @@ -0,0 +1,110 @@ +package oidc_e2e + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "syscall" + "testing" + "time" + + "github.com/chromedp/chromedp" + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "golang.org/x/crypto/bcrypt" +) + +var ( + serverURL string + serverPort int + serverCmd *exec.Cmd + chromectx context.Context + chromeCanc context.CancelFunc + tmpDir string +) + +func TestOIDCE2E(t *testing.T) { + RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "OIDC E2E") +} + +func freePort() int { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + port := l.Addr().(*net.TCPAddr).Port + l.Close() + return port +} + +var _ = ginkgo.BeforeSuite(func() { + serverPort = freePort() + serverURL = fmt.Sprintf("http://localhost:%d", serverPort) + + tmpDir = ginkgo.GinkgoT().TempDir() + + hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost) + Expect(err).ToNot(HaveOccurred()) + htpasswdPath := filepath.Join(tmpDir, "htpasswd") + Expect(os.WriteFile(htpasswdPath, fmt.Appendf(nil, "admin:%s\n", string(hash)), 0600)).To(Succeed()) + + dbPath := filepath.Join(tmpDir, ".db") + Expect(os.MkdirAll(dbPath, 0750)).To(Succeed()) + + binPath, err := filepath.Abs(".bin/incident-commander") + if err != nil { + binPath = ".bin/incident-commander" + } + if _, err := os.Stat(binPath); os.IsNotExist(err) { + // Try from project root + binPath, _ = filepath.Abs("../../.bin/incident-commander") + } + Expect(binPath).To(BeAnExistingFile(), "binary not found — run 'make dev' first") + + serverCmd = exec.Command(binPath, "serve", + "--db", fmt.Sprintf("embedded://%s", dbPath), + "--auth", "basic", + "--htpasswd-file", htpasswdPath, + "--oidc", + "--public-endpoint", serverURL, + "--httpPort", strconv.Itoa(serverPort), + "--disable-postgrest", + "--postgrest-uri", "", + "--disable-operators", + "--disable-kubernetes", + ) + serverCmd.Stdout = ginkgo.GinkgoWriter + serverCmd.Stderr = ginkgo.GinkgoWriter + serverCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + Expect(serverCmd.Start()).To(Succeed()) + + Eventually(func() error { + resp, err := http.Get(serverURL + "/health") + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health returned %d", resp.StatusCode) + } + return nil + }).WithTimeout(90 * time.Second).WithPolling(time.Second).Should(Succeed()) + + chromectx, chromeCanc = chromedp.NewContext(context.Background()) +}) + +var _ = ginkgo.AfterSuite(func() { + if chromeCanc != nil { + chromeCanc() + } + if serverCmd != nil && serverCmd.Process != nil { + _ = syscall.Kill(-serverCmd.Process.Pid, syscall.SIGTERM) + _ = serverCmd.Wait() + } +}) From fc80676689c34afb8c8d95b5fa141098fdc188d0 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 22 Mar 2026 15:37:09 +0200 Subject: [PATCH 12/48] fix(auth): normalize oidc issuer url to base public url Remove /oidc suffix from issuer URL construction so that the provider uses the base public URL (e.g., http://localhost:8080) instead of appending /oidc. Add static asset serving for embedded resources at /oidc/static/. --- auth/oidc/routes.go | 11 ++++++++++- auth/oidc_test.go | 16 ++++++++-------- auth/oidc_validate.go | 2 +- tests/oidc_e2e/oidc_login_test.go | 4 ++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index 00a010402..280131584 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -1,6 +1,10 @@ package oidc import ( + "io/fs" + "net/http" + "strings" + "github.com/flanksource/duty/context" "github.com/flanksource/incident-commander/auth/oidc/static" "github.com/labstack/echo/v4" @@ -11,7 +15,7 @@ import ( // so that discovery at /oidc/.well-known/openid-configuration returns correct endpoint URLs. // A convenience redirect from /.well-known/openid-configuration is provided for standard discovery. func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath string, checker CredentialChecker, lookup PersonLookup) error { - oidcIssuer := strings.TrimRight(issuerURL, "/") + "/oidc" + oidcIssuer := strings.TrimRight(issuerURL, "/") provider, err := NewProvider(ctx, oidcIssuer, signingKeyPath) if err != nil { return err @@ -36,5 +40,10 @@ func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath st e.Any("/endsession", h) e.Any("/.well-known/*", h) + // Serve embedded static assets (logo, tailwind) + staticFS, _ := fs.Sub(static.FS, ".") + staticHandler := http.StripPrefix("/oidc/static/", http.FileServer(http.FS(staticFS))) + e.GET("/oidc/static/*", echo.WrapHandler(staticHandler)) + return nil } diff --git a/auth/oidc_test.go b/auth/oidc_test.go index ba269712f..a6ff50365 100644 --- a/auth/oidc_test.go +++ b/auth/oidc_test.go @@ -275,7 +275,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080/oidc", + "iss": "http://localhost:8080", "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -301,7 +301,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080/oidc", + "iss": "http://localhost:8080", "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -328,7 +328,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080/oidc", + "iss": "http://localhost:8080", "aud": oidc.ClientID, "sub": person.ID.String(), "exp": time.Now().Add(-1 * time.Hour).Unix(), @@ -355,7 +355,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080/oidc", + "iss": "http://localhost:8080", "aud": "wrong-client", "sub": person.ID.String(), "exp": time.Now().Add(time.Hour).Unix(), @@ -382,7 +382,7 @@ var _ = ginkgo.Describe("OIDC", func() { defer func() { api.PublicURL = savedPublicURL }() claims := jwt.MapClaims{ - "iss": "http://localhost:8080/oidc", + "iss": "http://localhost:8080", "aud": oidc.ClientID, "sub": uuid.New().String(), // non-existent person "exp": time.Now().Add(time.Hour).Unix(), @@ -400,7 +400,7 @@ var _ = ginkgo.Describe("OIDC", func() { ginkgo.Describe("LoginHandler", func() { ginkgo.It("renders login form with auth_request_id", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080/oidc") + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080") e := newEchoInstance(DefaultContext) req := httptest.NewRequest(http.MethodGet, "/oidc/login?auth_request_id=test-123", nil) req = req.WithContext(DefaultContext.Wrap(req.Context())) @@ -414,7 +414,7 @@ var _ = ginkgo.Describe("OIDC", func() { }) ginkgo.It("returns 400 when auth_request_id is missing", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080/oidc") + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{}, mockLookup, "http://localhost:8080") e := newEchoInstance(DefaultContext) req := httptest.NewRequest(http.MethodGet, "/oidc/login", nil) req = req.WithContext(DefaultContext.Wrap(req.Context())) @@ -426,7 +426,7 @@ var _ = ginkgo.Describe("OIDC", func() { }) ginkgo.It("rejects invalid credentials", func() { - login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{valid: false}, mockLookup, "http://localhost:8080/oidc") + login := oidc.NewLoginHandler(provider.Storage, provider.OpenIDProvider, &mockChecker{valid: false}, mockLookup, "http://localhost:8080") e := newEchoInstance(DefaultContext) form := url.Values{ diff --git a/auth/oidc_validate.go b/auth/oidc_validate.go index 48b441785..499761d89 100644 --- a/auth/oidc_validate.go +++ b/auth/oidc_validate.go @@ -35,7 +35,7 @@ func authenticateOIDCToken(c echo.Context, tokenStr string) (bool, error) { return false, nil } - issuer := strings.TrimRight(api.PublicURL, "/") + "/oidc" + issuer := strings.TrimRight(api.PublicURL, "/") var lastErr error for _, pub := range keys { diff --git a/tests/oidc_e2e/oidc_login_test.go b/tests/oidc_e2e/oidc_login_test.go index eee444949..f1c605a12 100644 --- a/tests/oidc_e2e/oidc_login_test.go +++ b/tests/oidc_e2e/oidc_login_test.go @@ -106,7 +106,7 @@ var _ = ginkgo.Describe("OIDC Browser Login Flow", ginkgo.Label("slow"), ginkgo. Expect(oidcclient.ValidateNonce(tokens.IDToken, nonce)).To(Succeed()) - req, err := http.NewRequest("GET", serverURL+"/oidc/userinfo", nil) + req, err := http.NewRequest("GET", serverURL+"/userinfo", nil) Expect(err).ToNot(HaveOccurred()) req.Header.Set("Authorization", "Bearer "+tokens.AccessToken) @@ -130,7 +130,7 @@ var _ = ginkgo.Describe("OIDC Browser Login Flow", ginkgo.Label("slow"), ginkgo. Expect(refreshed.AccessToken).ToNot(Equal(originalAccess)) // Verify new access token works - req, err := http.NewRequest("GET", serverURL+"/oidc/userinfo", nil) + req, err := http.NewRequest("GET", serverURL+"/userinfo", nil) Expect(err).ToNot(HaveOccurred()) req.Header.Set("Authorization", "Bearer "+refreshed.AccessToken) From 160e171d4d862a61a525948abfe0916accd70ba1 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 22 Mar 2026 16:19:02 +0200 Subject: [PATCH 13/48] feat(auth): enhance oidc with styled login ui and token refresh support Adds Tailwind-styled login and success pages with embedded assets, proper token hashing in storage, crypto key management, and refresh token grant support. Extracts OIDC client logic to reusable package and adds browser-based e2e tests. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c51c006bd..51ac21daa 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ specs/ **/*.pem **/*.key auth/oidc/static/tailwind.min.js +**/*.pem +**/*.key From 300791dce7f5b01688554032063caad4c49a4fb4 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 23 Mar 2026 12:12:06 +0200 Subject: [PATCH 14/48] fix(ci): bump golangci-lint to v2.11.4 and add tailwind download to build targets golangci-lint v2.6.0 was built with Go 1.25 and cannot target Go 1.26. Add tailwind.min.js download as dependency for ci-test, e2e, and linux targets so the go:embed in auth/oidc/static/embed.go resolves in CI. --- Makefile | 5 +- auth/oidc/static/tailwind.min.js | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 auth/oidc/static/tailwind.min.js diff --git a/Makefile b/Makefile index c23d9f91d..f64021ccf 100644 --- a/Makefile +++ b/Makefile @@ -58,12 +58,11 @@ test: --succinct --label-filter='!ignore_local' .PHONY: ci-test -ci-test: - ginkgo -r -p --skip-package=tests/e2e --keep-going --junit-report junit-report.xml --github-output --output-dir test-reports --succinct +ci-test: $(TAILWIND_JS) + ginkgo -r --skip-package=tests/e2e --keep-going --junit-report junit-report.xml --github-output --output-dir test-reports --succinct .PHONY: e2e e2e: $(TAILWIND_JS) - go build -o ./.bin/$(NAME) main.go ginkgo -r --keep-going ./tests/e2e/... fmt: diff --git a/auth/oidc/static/tailwind.min.js b/auth/oidc/static/tailwind.min.js new file mode 100644 index 000000000..573c16591 --- /dev/null +++ b/auth/oidc/static/tailwind.min.js @@ -0,0 +1,83 @@ +(()=>{var qv=Object.create;var Hi=Object.defineProperty;var $v=Object.getOwnPropertyDescriptor;var Lv=Object.getOwnPropertyNames;var Mv=Object.getPrototypeOf,Nv=Object.prototype.hasOwnProperty;var df=r=>Hi(r,"__esModule",{value:!0});var hf=r=>{if(typeof require!="undefined")return require(r);throw new Error('Dynamic require of "'+r+'" is not supported')};var P=(r,e)=>()=>(r&&(e=r(r=0)),e);var x=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),Ge=(r,e)=>{df(r);for(var t in e)Hi(r,t,{get:e[t],enumerable:!0})},Bv=(r,e,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Lv(e))!Nv.call(r,i)&&i!=="default"&&Hi(r,i,{get:()=>e[i],enumerable:!(t=$v(e,i))||t.enumerable});return r},pe=r=>Bv(df(Hi(r!=null?qv(Mv(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var m,u=P(()=>{m={platform:"",env:{},versions:{node:"14.17.6"}}});var Fv,be,ft=P(()=>{u();Fv=0,be={readFileSync:r=>self[r]||"",statSync:()=>({mtimeMs:Fv++}),promises:{readFile:r=>Promise.resolve(self[r]||"")}}});var Fs=x((oP,gf)=>{u();"use strict";var mf=class{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");if(typeof e.maxAge=="number"&&e.maxAge===0)throw new TypeError("`maxAge` must be a number greater than 0");this.maxSize=e.maxSize,this.maxAge=e.maxAge||1/0,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_emitEvictions(e){if(typeof this.onEviction=="function")for(let[t,i]of e)this.onEviction(t,i.value)}_deleteIfExpired(e,t){return typeof t.expiry=="number"&&t.expiry<=Date.now()?(typeof this.onEviction=="function"&&this.onEviction(e,t.value),this.delete(e)):!1}_getOrDeleteIfExpired(e,t){if(this._deleteIfExpired(e,t)===!1)return t.value}_getItemValue(e,t){return t.expiry?this._getOrDeleteIfExpired(e,t):t.value}_peek(e,t){let i=t.get(e);return this._getItemValue(e,i)}_set(e,t){this.cache.set(e,t),this._size++,this._size>=this.maxSize&&(this._size=0,this._emitEvictions(this.oldCache),this.oldCache=this.cache,this.cache=new Map)}_moveToRecent(e,t){this.oldCache.delete(e),this._set(e,t)}*_entriesAscending(){for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield e)}for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield e)}}get(e){if(this.cache.has(e)){let t=this.cache.get(e);return this._getItemValue(e,t)}if(this.oldCache.has(e)){let t=this.oldCache.get(e);if(this._deleteIfExpired(e,t)===!1)return this._moveToRecent(e,t),t.value}}set(e,t,{maxAge:i=this.maxAge===1/0?void 0:Date.now()+this.maxAge}={}){this.cache.has(e)?this.cache.set(e,{value:t,maxAge:i}):this._set(e,{value:t,expiry:i})}has(e){return this.cache.has(e)?!this._deleteIfExpired(e,this.cache.get(e)):this.oldCache.has(e)?!this._deleteIfExpired(e,this.oldCache.get(e)):!1}peek(e){if(this.cache.has(e))return this._peek(e,this.cache);if(this.oldCache.has(e))return this._peek(e,this.oldCache)}delete(e){let t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}resize(e){if(!(e&&e>0))throw new TypeError("`maxSize` must be a number greater than 0");let t=[...this._entriesAscending()],i=t.length-e;i<0?(this.cache=new Map(t),this.oldCache=new Map,this._size=t.length):(i>0&&this._emitEvictions(t.slice(0,i)),this.oldCache=new Map(t.slice(i)),this.cache=new Map,this._size=0),this.maxSize=e}*keys(){for(let[e]of this)yield e}*values(){for(let[,e]of this)yield e}*[Symbol.iterator](){for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}}*entriesDescending(){let e=[...this.cache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,s]=i;this._deleteIfExpired(n,s)===!1&&(yield[n,s.value])}e=[...this.oldCache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,s]=i;this.cache.has(n)||this._deleteIfExpired(n,s)===!1&&(yield[n,s.value])}}*entriesAscending(){for(let[e,t]of this._entriesAscending())yield[e,t.value]}get size(){if(!this._size)return this.oldCache.size;let e=0;for(let t of this.oldCache.keys())this.cache.has(t)||e++;return Math.min(this._size+e,this.maxSize)}};gf.exports=mf});var yf,bf=P(()=>{u();yf=r=>r&&r._hash});function Wi(r){return yf(r,{ignoreUnknown:!0})}var wf=P(()=>{u();bf()});function xt(r){if(r=`${r}`,r==="0")return"0";if(/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(r))return r.replace(/^[+-]?/,t=>t==="-"?"":"-");let e=["var","calc","min","max","clamp"];for(let t of e)if(r.includes(`${t}(`))return`calc(${r} * -1)`}var Gi=P(()=>{u()});var vf,xf=P(()=>{u();vf=["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","lineClamp","display","aspectRatio","size","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","captionSide","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","listStyleImage","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","hyphens","whitespace","textWrap","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","contain","content","forcedColorAdjust"]});function kf(r,e){return r===void 0?e:Array.isArray(r)?r:[...new Set(e.filter(i=>r!==!1&&r[i]!==!1).concat(Object.keys(r).filter(i=>r[i]!==!1)))]}var Sf=P(()=>{u()});var Af={};Ge(Af,{default:()=>Qe});var Qe,Qi=P(()=>{u();Qe=new Proxy({},{get:()=>String})});function js(r,e,t){typeof m!="undefined"&&m.env.JEST_WORKER_ID||t&&Cf.has(t)||(t&&Cf.add(t),console.warn(""),e.forEach(i=>console.warn(r,"-",i)))}function zs(r){return Qe.dim(r)}var Cf,G,Be=P(()=>{u();Qi();Cf=new Set;G={info(r,e){js(Qe.bold(Qe.cyan("info")),...Array.isArray(r)?[r]:[e,r])},warn(r,e){["content-problems"].includes(r)||js(Qe.bold(Qe.yellow("warn")),...Array.isArray(r)?[r]:[e,r])},risk(r,e){js(Qe.bold(Qe.magenta("risk")),...Array.isArray(r)?[r]:[e,r])}}});var _f={};Ge(_f,{default:()=>Us});function qr({version:r,from:e,to:t}){G.warn(`${e}-color-renamed`,[`As of Tailwind CSS ${r}, \`${e}\` has been renamed to \`${t}\`.`,"Update your configuration file to silence this warning."])}var Us,Vs=P(()=>{u();Be();Us={inherit:"inherit",current:"currentColor",transparent:"transparent",black:"#000",white:"#fff",slate:{50:"#f8fafc",100:"#f1f5f9",200:"#e2e8f0",300:"#cbd5e1",400:"#94a3b8",500:"#64748b",600:"#475569",700:"#334155",800:"#1e293b",900:"#0f172a",950:"#020617"},gray:{50:"#f9fafb",100:"#f3f4f6",200:"#e5e7eb",300:"#d1d5db",400:"#9ca3af",500:"#6b7280",600:"#4b5563",700:"#374151",800:"#1f2937",900:"#111827",950:"#030712"},zinc:{50:"#fafafa",100:"#f4f4f5",200:"#e4e4e7",300:"#d4d4d8",400:"#a1a1aa",500:"#71717a",600:"#52525b",700:"#3f3f46",800:"#27272a",900:"#18181b",950:"#09090b"},neutral:{50:"#fafafa",100:"#f5f5f5",200:"#e5e5e5",300:"#d4d4d4",400:"#a3a3a3",500:"#737373",600:"#525252",700:"#404040",800:"#262626",900:"#171717",950:"#0a0a0a"},stone:{50:"#fafaf9",100:"#f5f5f4",200:"#e7e5e4",300:"#d6d3d1",400:"#a8a29e",500:"#78716c",600:"#57534e",700:"#44403c",800:"#292524",900:"#1c1917",950:"#0c0a09"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",300:"#fca5a5",400:"#f87171",500:"#ef4444",600:"#dc2626",700:"#b91c1c",800:"#991b1b",900:"#7f1d1d",950:"#450a0a"},orange:{50:"#fff7ed",100:"#ffedd5",200:"#fed7aa",300:"#fdba74",400:"#fb923c",500:"#f97316",600:"#ea580c",700:"#c2410c",800:"#9a3412",900:"#7c2d12",950:"#431407"},amber:{50:"#fffbeb",100:"#fef3c7",200:"#fde68a",300:"#fcd34d",400:"#fbbf24",500:"#f59e0b",600:"#d97706",700:"#b45309",800:"#92400e",900:"#78350f",950:"#451a03"},yellow:{50:"#fefce8",100:"#fef9c3",200:"#fef08a",300:"#fde047",400:"#facc15",500:"#eab308",600:"#ca8a04",700:"#a16207",800:"#854d0e",900:"#713f12",950:"#422006"},lime:{50:"#f7fee7",100:"#ecfccb",200:"#d9f99d",300:"#bef264",400:"#a3e635",500:"#84cc16",600:"#65a30d",700:"#4d7c0f",800:"#3f6212",900:"#365314",950:"#1a2e05"},green:{50:"#f0fdf4",100:"#dcfce7",200:"#bbf7d0",300:"#86efac",400:"#4ade80",500:"#22c55e",600:"#16a34a",700:"#15803d",800:"#166534",900:"#14532d",950:"#052e16"},emerald:{50:"#ecfdf5",100:"#d1fae5",200:"#a7f3d0",300:"#6ee7b7",400:"#34d399",500:"#10b981",600:"#059669",700:"#047857",800:"#065f46",900:"#064e3b",950:"#022c22"},teal:{50:"#f0fdfa",100:"#ccfbf1",200:"#99f6e4",300:"#5eead4",400:"#2dd4bf",500:"#14b8a6",600:"#0d9488",700:"#0f766e",800:"#115e59",900:"#134e4a",950:"#042f2e"},cyan:{50:"#ecfeff",100:"#cffafe",200:"#a5f3fc",300:"#67e8f9",400:"#22d3ee",500:"#06b6d4",600:"#0891b2",700:"#0e7490",800:"#155e75",900:"#164e63",950:"#083344"},sky:{50:"#f0f9ff",100:"#e0f2fe",200:"#bae6fd",300:"#7dd3fc",400:"#38bdf8",500:"#0ea5e9",600:"#0284c7",700:"#0369a1",800:"#075985",900:"#0c4a6e",950:"#082f49"},blue:{50:"#eff6ff",100:"#dbeafe",200:"#bfdbfe",300:"#93c5fd",400:"#60a5fa",500:"#3b82f6",600:"#2563eb",700:"#1d4ed8",800:"#1e40af",900:"#1e3a8a",950:"#172554"},indigo:{50:"#eef2ff",100:"#e0e7ff",200:"#c7d2fe",300:"#a5b4fc",400:"#818cf8",500:"#6366f1",600:"#4f46e5",700:"#4338ca",800:"#3730a3",900:"#312e81",950:"#1e1b4b"},violet:{50:"#f5f3ff",100:"#ede9fe",200:"#ddd6fe",300:"#c4b5fd",400:"#a78bfa",500:"#8b5cf6",600:"#7c3aed",700:"#6d28d9",800:"#5b21b6",900:"#4c1d95",950:"#2e1065"},purple:{50:"#faf5ff",100:"#f3e8ff",200:"#e9d5ff",300:"#d8b4fe",400:"#c084fc",500:"#a855f7",600:"#9333ea",700:"#7e22ce",800:"#6b21a8",900:"#581c87",950:"#3b0764"},fuchsia:{50:"#fdf4ff",100:"#fae8ff",200:"#f5d0fe",300:"#f0abfc",400:"#e879f9",500:"#d946ef",600:"#c026d3",700:"#a21caf",800:"#86198f",900:"#701a75",950:"#4a044e"},pink:{50:"#fdf2f8",100:"#fce7f3",200:"#fbcfe8",300:"#f9a8d4",400:"#f472b6",500:"#ec4899",600:"#db2777",700:"#be185d",800:"#9d174d",900:"#831843",950:"#500724"},rose:{50:"#fff1f2",100:"#ffe4e6",200:"#fecdd3",300:"#fda4af",400:"#fb7185",500:"#f43f5e",600:"#e11d48",700:"#be123c",800:"#9f1239",900:"#881337",950:"#4c0519"},get lightBlue(){return qr({version:"v2.2",from:"lightBlue",to:"sky"}),this.sky},get warmGray(){return qr({version:"v3.0",from:"warmGray",to:"stone"}),this.stone},get trueGray(){return qr({version:"v3.0",from:"trueGray",to:"neutral"}),this.neutral},get coolGray(){return qr({version:"v3.0",from:"coolGray",to:"gray"}),this.gray},get blueGray(){return qr({version:"v3.0",from:"blueGray",to:"slate"}),this.slate}}});function Hs(r,...e){for(let t of e){for(let i in t)r?.hasOwnProperty?.(i)||(r[i]=t[i]);for(let i of Object.getOwnPropertySymbols(t))r?.hasOwnProperty?.(i)||(r[i]=t[i])}return r}var Ef=P(()=>{u()});function kt(r){if(Array.isArray(r))return r;let e=r.split("[").length-1,t=r.split("]").length-1;if(e!==t)throw new Error(`Path is invalid. Has unbalanced brackets: ${r}`);return r.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)}var Yi=P(()=>{u()});function we(r,e){return Ki.future.includes(e)?r.future==="all"||(r?.future?.[e]??Of[e]??!1):Ki.experimental.includes(e)?r.experimental==="all"||(r?.experimental?.[e]??Of[e]??!1):!1}function Tf(r){return r.experimental==="all"?Ki.experimental:Object.keys(r?.experimental??{}).filter(e=>Ki.experimental.includes(e)&&r.experimental[e])}function Rf(r){if(m.env.JEST_WORKER_ID===void 0&&Tf(r).length>0){let e=Tf(r).map(t=>Qe.yellow(t)).join(", ");G.warn("experimental-flags-enabled",[`You have enabled experimental features: ${e}`,"Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time."])}}var Of,Ki,ct=P(()=>{u();Qi();Be();Of={optimizeUniversalDefaults:!1,generalizedModifiers:!0,disableColorOpacityUtilitiesByDefault:!1,relativeContentPathsByDefault:!1},Ki={future:["hoverOnlyWhenSupported","respectDefaultRingColorOpacity","disableColorOpacityUtilitiesByDefault","relativeContentPathsByDefault"],experimental:["optimizeUniversalDefaults","generalizedModifiers"]}});function Pf(r){(()=>{if(r.purge||!r.content||!Array.isArray(r.content)&&!(typeof r.content=="object"&&r.content!==null))return!1;if(Array.isArray(r.content))return r.content.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string"));if(typeof r.content=="object"&&r.content!==null){if(Object.keys(r.content).some(t=>!["files","relative","extract","transform"].includes(t)))return!1;if(Array.isArray(r.content.files)){if(!r.content.files.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string")))return!1;if(typeof r.content.extract=="object"){for(let t of Object.values(r.content.extract))if(typeof t!="function")return!1}else if(!(r.content.extract===void 0||typeof r.content.extract=="function"))return!1;if(typeof r.content.transform=="object"){for(let t of Object.values(r.content.transform))if(typeof t!="function")return!1}else if(!(r.content.transform===void 0||typeof r.content.transform=="function"))return!1;if(typeof r.content.relative!="boolean"&&typeof r.content.relative!="undefined")return!1}return!0}return!1})()||G.warn("purge-deprecation",["The `purge`/`content` options have changed in Tailwind CSS v3.0.","Update your configuration file to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#configure-content-sources"]),r.safelist=(()=>{let{content:t,purge:i,safelist:n}=r;return Array.isArray(n)?n:Array.isArray(t?.safelist)?t.safelist:Array.isArray(i?.safelist)?i.safelist:Array.isArray(i?.options?.safelist)?i.options.safelist:[]})(),r.blocklist=(()=>{let{blocklist:t}=r;if(Array.isArray(t)){if(t.every(i=>typeof i=="string"))return t;G.warn("blocklist-invalid",["The `blocklist` option must be an array of strings.","https://tailwindcss.com/docs/content-configuration#discarding-classes"])}return[]})(),typeof r.prefix=="function"?(G.warn("prefix-function",["As of Tailwind CSS v3.0, `prefix` cannot be a function.","Update `prefix` in your configuration to be a string to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function"]),r.prefix=""):r.prefix=r.prefix??"",r.content={relative:(()=>{let{content:t}=r;return t?.relative?t.relative:we(r,"relativeContentPathsByDefault")})(),files:(()=>{let{content:t,purge:i}=r;return Array.isArray(i)?i:Array.isArray(i?.content)?i.content:Array.isArray(t)?t:Array.isArray(t?.content)?t.content:Array.isArray(t?.files)?t.files:[]})(),extract:(()=>{let t=(()=>r.purge?.extract?r.purge.extract:r.content?.extract?r.content.extract:r.purge?.extract?.DEFAULT?r.purge.extract.DEFAULT:r.content?.extract?.DEFAULT?r.content.extract.DEFAULT:r.purge?.options?.extractors?r.purge.options.extractors:r.content?.options?.extractors?r.content.options.extractors:{})(),i={},n=(()=>{if(r.purge?.options?.defaultExtractor)return r.purge.options.defaultExtractor;if(r.content?.options?.defaultExtractor)return r.content.options.defaultExtractor})();if(n!==void 0&&(i.DEFAULT=n),typeof t=="function")i.DEFAULT=t;else if(Array.isArray(t))for(let{extensions:s,extractor:a}of t??[])for(let o of s)i[o]=a;else typeof t=="object"&&t!==null&&Object.assign(i,t);return i})(),transform:(()=>{let t=(()=>r.purge?.transform?r.purge.transform:r.content?.transform?r.content.transform:r.purge?.transform?.DEFAULT?r.purge.transform.DEFAULT:r.content?.transform?.DEFAULT?r.content.transform.DEFAULT:{})(),i={};return typeof t=="function"?i.DEFAULT=t:typeof t=="object"&&t!==null&&Object.assign(i,t),i})()};for(let t of r.content.files)if(typeof t=="string"&&/{([^,]*?)}/g.test(t)){G.warn("invalid-glob-braces",[`The glob pattern ${zs(t)} in your Tailwind CSS configuration is invalid.`,`Update it to ${zs(t.replace(/{([^,]*?)}/g,"$1"))} to silence this warning.`]);break}return r}var If=P(()=>{u();ct();Be()});function ke(r){if(Object.prototype.toString.call(r)!=="[object Object]")return!1;let e=Object.getPrototypeOf(r);return e===null||Object.getPrototypeOf(e)===null}var Kt=P(()=>{u()});function St(r){return Array.isArray(r)?r.map(e=>St(e)):typeof r=="object"&&r!==null?Object.fromEntries(Object.entries(r).map(([e,t])=>[e,St(t)])):r}var Xi=P(()=>{u()});function jt(r){return r.replace(/\\,/g,"\\2c ")}var Zi=P(()=>{u()});var Ws,Df=P(()=>{u();Ws={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});function $r(r,{loose:e=!1}={}){if(typeof r!="string")return null;if(r=r.trim(),r==="transparent")return{mode:"rgb",color:["0","0","0"],alpha:"0"};if(r in Ws)return{mode:"rgb",color:Ws[r].map(s=>s.toString())};let t=r.replace(zv,(s,a,o,l,c)=>["#",a,a,o,o,l,l,c?c+c:""].join("")).match(jv);if(t!==null)return{mode:"rgb",color:[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)].map(s=>s.toString()),alpha:t[4]?(parseInt(t[4],16)/255).toString():void 0};let i=r.match(Uv)??r.match(Vv);if(i===null)return null;let n=[i[2],i[3],i[4]].filter(Boolean).map(s=>s.toString());return n.length===2&&n[0].startsWith("var(")?{mode:i[1],color:[n[0]],alpha:n[1]}:!e&&n.length!==3||n.length<3&&!n.some(s=>/^var\(.*?\)$/.test(s))?null:{mode:i[1],color:n,alpha:i[5]?.toString?.()}}function Gs({mode:r,color:e,alpha:t}){let i=t!==void 0;return r==="rgba"||r==="hsla"?`${r}(${e.join(", ")}${i?`, ${t}`:""})`:`${r}(${e.join(" ")}${i?` / ${t}`:""})`}var jv,zv,At,Ji,qf,Ct,Uv,Vv,Qs=P(()=>{u();Df();jv=/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i,zv=/^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i,At=/(?:\d+|\d*\.\d+)%?/,Ji=/(?:\s*,\s*|\s+)/,qf=/\s*[,/]\s*/,Ct=/var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/,Uv=new RegExp(`^(rgba?)\\(\\s*(${At.source}|${Ct.source})(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${qf.source}(${At.source}|${Ct.source}))?\\s*\\)$`),Vv=new RegExp(`^(hsla?)\\(\\s*((?:${At.source})(?:deg|rad|grad|turn)?|${Ct.source})(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${qf.source}(${At.source}|${Ct.source}))?\\s*\\)$`)});function Je(r,e,t){if(typeof r=="function")return r({opacityValue:e});let i=$r(r,{loose:!0});return i===null?t:Gs({...i,alpha:e})}function Ae({color:r,property:e,variable:t}){let i=[].concat(e);if(typeof r=="function")return{[t]:"1",...Object.fromEntries(i.map(s=>[s,r({opacityVariable:t,opacityValue:`var(${t}, 1)`})]))};let n=$r(r);return n===null?Object.fromEntries(i.map(s=>[s,r])):n.alpha!==void 0?Object.fromEntries(i.map(s=>[s,r])):{[t]:"1",...Object.fromEntries(i.map(s=>[s,Gs({...n,alpha:`var(${t}, 1)`})]))}}var Lr=P(()=>{u();Qs()});function ve(r,e){let t=[],i=[],n=0,s=!1;for(let a=0;a{u()});function en(r){return ve(r,",").map(t=>{let i=t.trim(),n={raw:i},s=i.split(Wv),a=new Set;for(let o of s)$f.lastIndex=0,!a.has("KEYWORD")&&Hv.has(o)?(n.keyword=o,a.add("KEYWORD")):$f.test(o)?a.has("X")?a.has("Y")?a.has("BLUR")?a.has("SPREAD")||(n.spread=o,a.add("SPREAD")):(n.blur=o,a.add("BLUR")):(n.y=o,a.add("Y")):(n.x=o,a.add("X")):n.color?(n.unknown||(n.unknown=[]),n.unknown.push(o)):n.color=o;return n.valid=n.x!==void 0&&n.y!==void 0,n})}function Lf(r){return r.map(e=>e.valid?[e.keyword,e.x,e.y,e.blur,e.spread,e.color].filter(Boolean).join(" "):e.raw).join(", ")}var Hv,Wv,$f,Ys=P(()=>{u();zt();Hv=new Set(["inset","inherit","initial","revert","unset"]),Wv=/\ +(?![^(]*\))/g,$f=/^-?(\d+|\.\d+)(.*?)$/g});function Ks(r){return Gv.some(e=>new RegExp(`^${e}\\(.*\\)`).test(r))}function K(r,e=null,t=!0){let i=e&&Qv.has(e.property);return r.startsWith("--")&&!i?`var(${r})`:r.includes("url(")?r.split(/(url\(.*?\))/g).filter(Boolean).map(n=>/^url\(.*?\)$/.test(n)?n:K(n,e,!1)).join(""):(r=r.replace(/([^\\])_+/g,(n,s)=>s+" ".repeat(n.length-1)).replace(/^_/g," ").replace(/\\_/g,"_"),t&&(r=r.trim()),r=Yv(r),r)}function Ye(r){return r.includes("=")&&(r=r.replace(/(=.*)/g,(e,t)=>{if(t[1]==="'"||t[1]==='"')return t;if(t.length>2){let i=t[t.length-1];if(t[t.length-2]===" "&&(i==="i"||i==="I"||i==="s"||i==="S"))return`="${t.slice(1,-2)}" ${t[t.length-1]}`}return`="${t.slice(1)}"`})),r}function Yv(r){let e=["theme"],t=["min-content","max-content","fit-content","safe-area-inset-top","safe-area-inset-right","safe-area-inset-bottom","safe-area-inset-left","titlebar-area-x","titlebar-area-y","titlebar-area-width","titlebar-area-height","keyboard-inset-top","keyboard-inset-right","keyboard-inset-bottom","keyboard-inset-left","keyboard-inset-width","keyboard-inset-height","radial-gradient","linear-gradient","conic-gradient","repeating-radial-gradient","repeating-linear-gradient","repeating-conic-gradient","anchor-size"];return r.replace(/(calc|min|max|clamp)\(.+\)/g,i=>{let n="";function s(){let a=n.trimEnd();return a[a.length-1]}for(let a=0;ai[a+p]===d)},l=function(f){let d=1/0;for(let h of f){let b=i.indexOf(h,a);b!==-1&&bo(f))){let f=t.find(d=>o(d));n+=f,a+=f.length-1}else e.some(f=>o(f))?n+=l([")"]):o("[")?n+=l(["]"]):["+","-","*","/"].includes(c)&&!["(","+","-","*","/",","].includes(s())?n+=` ${c} `:n+=c}return n.replace(/\s+/g," ")})}function Xs(r){return r.startsWith("url(")}function Zs(r){return!isNaN(Number(r))||Ks(r)}function Mr(r){return r.endsWith("%")&&Zs(r.slice(0,-1))||Ks(r)}function Nr(r){return r==="0"||new RegExp(`^[+-]?[0-9]*.?[0-9]+(?:[eE][+-]?[0-9]+)?${Xv}$`).test(r)||Ks(r)}function Mf(r){return Zv.has(r)}function Nf(r){let e=en(K(r));for(let t of e)if(!t.valid)return!1;return!0}function Bf(r){let e=0;return ve(r,"_").every(i=>(i=K(i),i.startsWith("var(")?!0:$r(i,{loose:!0})!==null?(e++,!0):!1))?e>0:!1}function Ff(r){let e=0;return ve(r,",").every(i=>(i=K(i),i.startsWith("var(")?!0:Xs(i)||ex(i)||["element(","image(","cross-fade(","image-set("].some(n=>i.startsWith(n))?(e++,!0):!1))?e>0:!1}function ex(r){r=K(r);for(let e of Jv)if(r.startsWith(`${e}(`))return!0;return!1}function jf(r){let e=0;return ve(r,"_").every(i=>(i=K(i),i.startsWith("var(")?!0:tx.has(i)||Nr(i)||Mr(i)?(e++,!0):!1))?e>0:!1}function zf(r){let e=0;return ve(r,",").every(i=>(i=K(i),i.startsWith("var(")?!0:i.includes(" ")&&!/(['"])([^"']+)\1/g.test(i)||/^\d/g.test(i)?!1:(e++,!0)))?e>0:!1}function Uf(r){return rx.has(r)}function Vf(r){return ix.has(r)}function Hf(r){return nx.has(r)}var Gv,Qv,Kv,Xv,Zv,Jv,tx,rx,ix,nx,Br=P(()=>{u();Qs();Ys();zt();Gv=["min","max","clamp","calc"];Qv=new Set(["scroll-timeline-name","timeline-scope","view-timeline-name","font-palette","anchor-name","anchor-scope","position-anchor","position-try-options","scroll-timeline","animation-timeline","view-timeline","position-try"]);Kv=["cm","mm","Q","in","pc","pt","px","em","ex","ch","rem","lh","rlh","vw","vh","vmin","vmax","vb","vi","svw","svh","lvw","lvh","dvw","dvh","cqw","cqh","cqi","cqb","cqmin","cqmax"],Xv=`(?:${Kv.join("|")})`;Zv=new Set(["thin","medium","thick"]);Jv=new Set(["conic-gradient","linear-gradient","radial-gradient","repeating-conic-gradient","repeating-linear-gradient","repeating-radial-gradient"]);tx=new Set(["center","top","right","bottom","left"]);rx=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-serif","ui-sans-serif","ui-monospace","ui-rounded","math","emoji","fangsong"]);ix=new Set(["xx-small","x-small","small","medium","large","x-large","xx-large","xxx-large"]);nx=new Set(["larger","smaller"])});function Wf(r){let e=["cover","contain"];return ve(r,",").every(t=>{let i=ve(t,"_").filter(Boolean);return i.length===1&&e.includes(i[0])?!0:i.length!==1&&i.length!==2?!1:i.every(n=>Nr(n)||Mr(n)||n==="auto")})}var Gf=P(()=>{u();Br();zt()});function Qf(r,e){r.walkClasses(t=>{t.value=e(t.value),t.raws&&t.raws.value&&(t.raws.value=jt(t.raws.value))})}function Yf(r,e){if(!_t(r))return;let t=r.slice(1,-1);if(!!e(t))return K(t)}function sx(r,e={},t){let i=e[r];if(i!==void 0)return xt(i);if(_t(r)){let n=Yf(r,t);return n===void 0?void 0:xt(n)}}function tn(r,e={},{validate:t=()=>!0}={}){let i=e.values?.[r];return i!==void 0?i:e.supportsNegativeValues&&r.startsWith("-")?sx(r.slice(1),e.values,t):Yf(r,t)}function _t(r){return r.startsWith("[")&&r.endsWith("]")}function Kf(r){let e=r.lastIndexOf("/"),t=r.lastIndexOf("[",e),i=r.indexOf("]",e);return r[e-1]==="]"||r[e+1]==="["||t!==-1&&i!==-1&&t")){let e=r;return({opacityValue:t=1})=>e.replace(//g,t)}return r}function Xf(r){return K(r.slice(1,-1))}function ax(r,e={},{tailwindConfig:t={}}={}){if(e.values?.[r]!==void 0)return Xt(e.values?.[r]);let[i,n]=Kf(r);if(n!==void 0){let s=e.values?.[i]??(_t(i)?i.slice(1,-1):void 0);return s===void 0?void 0:(s=Xt(s),_t(n)?Je(s,Xf(n)):t.theme?.opacity?.[n]===void 0?void 0:Je(s,t.theme.opacity[n]))}return tn(r,e,{validate:Bf})}function ox(r,e={}){return e.values?.[r]}function qe(r){return(e,t)=>tn(e,t,{validate:r})}function lx(r,e){let t=r.indexOf(e);return t===-1?[void 0,r]:[r.slice(0,t),r.slice(t+1)]}function ea(r,e,t,i){if(t.values&&e in t.values)for(let{type:s}of r??[]){let a=Js[s](e,t,{tailwindConfig:i});if(a!==void 0)return[a,s,null]}if(_t(e)){let s=e.slice(1,-1),[a,o]=lx(s,":");if(!/^[\w-_]+$/g.test(a))o=s;else if(a!==void 0&&!Zf.includes(a))return[];if(o.length>0&&Zf.includes(a))return[tn(`[${o}]`,t),a,null]}let n=ta(r,e,t,i);for(let s of n)return s;return[]}function*ta(r,e,t,i){let n=we(i,"generalizedModifiers"),[s,a]=Kf(e);if(n&&t.modifiers!=null&&(t.modifiers==="any"||typeof t.modifiers=="object"&&(a&&_t(a)||a in t.modifiers))||(s=e,a=void 0),a!==void 0&&s===""&&(s="DEFAULT"),a!==void 0&&typeof t.modifiers=="object"){let l=t.modifiers?.[a]??null;l!==null?a=l:_t(a)&&(a=Xf(a))}for(let{type:l}of r??[]){let c=Js[l](s,t,{tailwindConfig:i});c!==void 0&&(yield[c,l,a??null])}}var Js,Zf,Fr=P(()=>{u();Zi();Lr();Br();Gi();Gf();ct();Js={any:tn,color:ax,url:qe(Xs),image:qe(Ff),length:qe(Nr),percentage:qe(Mr),position:qe(jf),lookup:ox,"generic-name":qe(Uf),"family-name":qe(zf),number:qe(Zs),"line-width":qe(Mf),"absolute-size":qe(Vf),"relative-size":qe(Hf),shadow:qe(Nf),size:qe(Wf)},Zf=Object.keys(Js)});function X(r){return typeof r=="function"?r({}):r}var ra=P(()=>{u()});function Zt(r){return typeof r=="function"}function jr(r,...e){let t=e.pop();for(let i of e)for(let n in i){let s=t(r[n],i[n]);s===void 0?ke(r[n])&&ke(i[n])?r[n]=jr({},r[n],i[n],t):r[n]=i[n]:r[n]=s}return r}function ux(r,...e){return Zt(r)?r(...e):r}function fx(r){return r.reduce((e,{extend:t})=>jr(e,t,(i,n)=>i===void 0?[n]:Array.isArray(i)?[n,...i]:[n,i]),{})}function cx(r){return{...r.reduce((e,t)=>Hs(e,t),{}),extend:fx(r)}}function Jf(r,e){if(Array.isArray(r)&&ke(r[0]))return r.concat(e);if(Array.isArray(e)&&ke(e[0])&&ke(r))return[r,...e];if(Array.isArray(e))return e}function px({extend:r,...e}){return jr(e,r,(t,i)=>!Zt(t)&&!i.some(Zt)?jr({},t,...i,Jf):(n,s)=>jr({},...[t,...i].map(a=>ux(a,n,s)),Jf))}function*dx(r){let e=kt(r);if(e.length===0||(yield e,Array.isArray(r)))return;let t=/^(.*?)\s*\/\s*([^/]+)$/,i=r.match(t);if(i!==null){let[,n,s]=i,a=kt(n);a.alpha=s,yield a}}function hx(r){let e=(t,i)=>{for(let n of dx(t)){let s=0,a=r;for(;a!=null&&s(t[i]=Zt(r[i])?r[i](e,ia):r[i],t),{})}function ec(r){let e=[];return r.forEach(t=>{e=[...e,t];let i=t?.plugins??[];i.length!==0&&i.forEach(n=>{n.__isOptionsFunction&&(n=n()),e=[...e,...ec([n?.config??{}])]})}),e}function mx(r){return[...r].reduceRight((t,i)=>Zt(i)?i({corePlugins:t}):kf(i,t),vf)}function gx(r){return[...r].reduceRight((t,i)=>[...t,...i],[])}function na(r){let e=[...ec(r),{prefix:"",important:!1,separator:":"}];return Pf(Hs({theme:hx(px(cx(e.map(t=>t?.theme??{})))),corePlugins:mx(e.map(t=>t.corePlugins)),plugins:gx(r.map(t=>t?.plugins??[]))},...e))}var ia,tc=P(()=>{u();Gi();xf();Sf();Vs();Ef();Yi();If();Kt();Xi();Fr();Lr();ra();ia={colors:Us,negative(r){return Object.keys(r).filter(e=>r[e]!=="0").reduce((e,t)=>{let i=xt(r[t]);return i!==void 0&&(e[`-${t}`]=i),e},{})},breakpoints(r){return Object.keys(r).filter(e=>typeof r[e]=="string").reduce((e,t)=>({...e,[`screen-${t}`]:r[t]}),{})}}});var rn=x((f3,rc)=>{u();rc.exports={content:[],presets:[],darkMode:"media",theme:{accentColor:({theme:r})=>({...r("colors"),auto:"auto"}),animation:{none:"none",spin:"spin 1s linear infinite",ping:"ping 1s cubic-bezier(0, 0, 0.2, 1) infinite",pulse:"pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",bounce:"bounce 1s infinite"},aria:{busy:'busy="true"',checked:'checked="true"',disabled:'disabled="true"',expanded:'expanded="true"',hidden:'hidden="true"',pressed:'pressed="true"',readonly:'readonly="true"',required:'required="true"',selected:'selected="true"'},aspectRatio:{auto:"auto",square:"1 / 1",video:"16 / 9"},backdropBlur:({theme:r})=>r("blur"),backdropBrightness:({theme:r})=>r("brightness"),backdropContrast:({theme:r})=>r("contrast"),backdropGrayscale:({theme:r})=>r("grayscale"),backdropHueRotate:({theme:r})=>r("hueRotate"),backdropInvert:({theme:r})=>r("invert"),backdropOpacity:({theme:r})=>r("opacity"),backdropSaturate:({theme:r})=>r("saturate"),backdropSepia:({theme:r})=>r("sepia"),backgroundColor:({theme:r})=>r("colors"),backgroundImage:{none:"none","gradient-to-t":"linear-gradient(to top, var(--tw-gradient-stops))","gradient-to-tr":"linear-gradient(to top right, var(--tw-gradient-stops))","gradient-to-r":"linear-gradient(to right, var(--tw-gradient-stops))","gradient-to-br":"linear-gradient(to bottom right, var(--tw-gradient-stops))","gradient-to-b":"linear-gradient(to bottom, var(--tw-gradient-stops))","gradient-to-bl":"linear-gradient(to bottom left, var(--tw-gradient-stops))","gradient-to-l":"linear-gradient(to left, var(--tw-gradient-stops))","gradient-to-tl":"linear-gradient(to top left, var(--tw-gradient-stops))"},backgroundOpacity:({theme:r})=>r("opacity"),backgroundPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},backgroundSize:{auto:"auto",cover:"cover",contain:"contain"},blur:{0:"0",none:"",sm:"4px",DEFAULT:"8px",md:"12px",lg:"16px",xl:"24px","2xl":"40px","3xl":"64px"},borderColor:({theme:r})=>({...r("colors"),DEFAULT:r("colors.gray.200","currentColor")}),borderOpacity:({theme:r})=>r("opacity"),borderRadius:{none:"0px",sm:"0.125rem",DEFAULT:"0.25rem",md:"0.375rem",lg:"0.5rem",xl:"0.75rem","2xl":"1rem","3xl":"1.5rem",full:"9999px"},borderSpacing:({theme:r})=>({...r("spacing")}),borderWidth:{DEFAULT:"1px",0:"0px",2:"2px",4:"4px",8:"8px"},boxShadow:{sm:"0 1px 2px 0 rgb(0 0 0 / 0.05)",DEFAULT:"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",md:"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",lg:"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",xl:"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)","2xl":"0 25px 50px -12px rgb(0 0 0 / 0.25)",inner:"inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",none:"none"},boxShadowColor:({theme:r})=>r("colors"),brightness:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5",200:"2"},caretColor:({theme:r})=>r("colors"),colors:({colors:r})=>({inherit:r.inherit,current:r.current,transparent:r.transparent,black:r.black,white:r.white,slate:r.slate,gray:r.gray,zinc:r.zinc,neutral:r.neutral,stone:r.stone,red:r.red,orange:r.orange,amber:r.amber,yellow:r.yellow,lime:r.lime,green:r.green,emerald:r.emerald,teal:r.teal,cyan:r.cyan,sky:r.sky,blue:r.blue,indigo:r.indigo,violet:r.violet,purple:r.purple,fuchsia:r.fuchsia,pink:r.pink,rose:r.rose}),columns:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12","3xs":"16rem","2xs":"18rem",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem"},container:{},content:{none:"none"},contrast:{0:"0",50:".5",75:".75",100:"1",125:"1.25",150:"1.5",200:"2"},cursor:{auto:"auto",default:"default",pointer:"pointer",wait:"wait",text:"text",move:"move",help:"help","not-allowed":"not-allowed",none:"none","context-menu":"context-menu",progress:"progress",cell:"cell",crosshair:"crosshair","vertical-text":"vertical-text",alias:"alias",copy:"copy","no-drop":"no-drop",grab:"grab",grabbing:"grabbing","all-scroll":"all-scroll","col-resize":"col-resize","row-resize":"row-resize","n-resize":"n-resize","e-resize":"e-resize","s-resize":"s-resize","w-resize":"w-resize","ne-resize":"ne-resize","nw-resize":"nw-resize","se-resize":"se-resize","sw-resize":"sw-resize","ew-resize":"ew-resize","ns-resize":"ns-resize","nesw-resize":"nesw-resize","nwse-resize":"nwse-resize","zoom-in":"zoom-in","zoom-out":"zoom-out"},divideColor:({theme:r})=>r("borderColor"),divideOpacity:({theme:r})=>r("borderOpacity"),divideWidth:({theme:r})=>r("borderWidth"),dropShadow:{sm:"0 1px 1px rgb(0 0 0 / 0.05)",DEFAULT:["0 1px 2px rgb(0 0 0 / 0.1)","0 1px 1px rgb(0 0 0 / 0.06)"],md:["0 4px 3px rgb(0 0 0 / 0.07)","0 2px 2px rgb(0 0 0 / 0.06)"],lg:["0 10px 8px rgb(0 0 0 / 0.04)","0 4px 3px rgb(0 0 0 / 0.1)"],xl:["0 20px 13px rgb(0 0 0 / 0.03)","0 8px 5px rgb(0 0 0 / 0.08)"],"2xl":"0 25px 25px rgb(0 0 0 / 0.15)",none:"0 0 #0000"},fill:({theme:r})=>({none:"none",...r("colors")}),flex:{1:"1 1 0%",auto:"1 1 auto",initial:"0 1 auto",none:"none"},flexBasis:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%"}),flexGrow:{0:"0",DEFAULT:"1"},flexShrink:{0:"0",DEFAULT:"1"},fontFamily:{sans:["ui-sans-serif","system-ui","sans-serif",'"Apple Color Emoji"','"Segoe UI Emoji"','"Segoe UI Symbol"','"Noto Color Emoji"'],serif:["ui-serif","Georgia","Cambria",'"Times New Roman"',"Times","serif"],mono:["ui-monospace","SFMono-Regular","Menlo","Monaco","Consolas",'"Liberation Mono"','"Courier New"',"monospace"]},fontSize:{xs:["0.75rem",{lineHeight:"1rem"}],sm:["0.875rem",{lineHeight:"1.25rem"}],base:["1rem",{lineHeight:"1.5rem"}],lg:["1.125rem",{lineHeight:"1.75rem"}],xl:["1.25rem",{lineHeight:"1.75rem"}],"2xl":["1.5rem",{lineHeight:"2rem"}],"3xl":["1.875rem",{lineHeight:"2.25rem"}],"4xl":["2.25rem",{lineHeight:"2.5rem"}],"5xl":["3rem",{lineHeight:"1"}],"6xl":["3.75rem",{lineHeight:"1"}],"7xl":["4.5rem",{lineHeight:"1"}],"8xl":["6rem",{lineHeight:"1"}],"9xl":["8rem",{lineHeight:"1"}]},fontWeight:{thin:"100",extralight:"200",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},gap:({theme:r})=>r("spacing"),gradientColorStops:({theme:r})=>r("colors"),gradientColorStopPositions:{"0%":"0%","5%":"5%","10%":"10%","15%":"15%","20%":"20%","25%":"25%","30%":"30%","35%":"35%","40%":"40%","45%":"45%","50%":"50%","55%":"55%","60%":"60%","65%":"65%","70%":"70%","75%":"75%","80%":"80%","85%":"85%","90%":"90%","95%":"95%","100%":"100%"},grayscale:{0:"0",DEFAULT:"100%"},gridAutoColumns:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridAutoRows:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridColumn:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridColumnEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridColumnStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRow:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridRowEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRowStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridTemplateColumns:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},gridTemplateRows:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},height:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),hueRotate:{0:"0deg",15:"15deg",30:"30deg",60:"60deg",90:"90deg",180:"180deg"},inset:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),invert:{0:"0",DEFAULT:"100%"},keyframes:{spin:{to:{transform:"rotate(360deg)"}},ping:{"75%, 100%":{transform:"scale(2)",opacity:"0"}},pulse:{"50%":{opacity:".5"}},bounce:{"0%, 100%":{transform:"translateY(-25%)",animationTimingFunction:"cubic-bezier(0.8,0,1,1)"},"50%":{transform:"none",animationTimingFunction:"cubic-bezier(0,0,0.2,1)"}}},letterSpacing:{tighter:"-0.05em",tight:"-0.025em",normal:"0em",wide:"0.025em",wider:"0.05em",widest:"0.1em"},lineHeight:{none:"1",tight:"1.25",snug:"1.375",normal:"1.5",relaxed:"1.625",loose:"2",3:".75rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem"},listStyleType:{none:"none",disc:"disc",decimal:"decimal"},listStyleImage:{none:"none"},margin:({theme:r})=>({auto:"auto",...r("spacing")}),lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"},maxHeight:({theme:r})=>({...r("spacing"),none:"none",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),maxWidth:({theme:r,breakpoints:e})=>({...r("spacing"),none:"none",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem",full:"100%",min:"min-content",max:"max-content",fit:"fit-content",prose:"65ch",...e(r("screens"))}),minHeight:({theme:r})=>({...r("spacing"),full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),minWidth:({theme:r})=>({...r("spacing"),full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),objectPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},opacity:{0:"0",5:"0.05",10:"0.1",15:"0.15",20:"0.2",25:"0.25",30:"0.3",35:"0.35",40:"0.4",45:"0.45",50:"0.5",55:"0.55",60:"0.6",65:"0.65",70:"0.7",75:"0.75",80:"0.8",85:"0.85",90:"0.9",95:"0.95",100:"1"},order:{first:"-9999",last:"9999",none:"0",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12"},outlineColor:({theme:r})=>r("colors"),outlineOffset:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},outlineWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},padding:({theme:r})=>r("spacing"),placeholderColor:({theme:r})=>r("colors"),placeholderOpacity:({theme:r})=>r("opacity"),ringColor:({theme:r})=>({DEFAULT:r("colors.blue.500","#3b82f6"),...r("colors")}),ringOffsetColor:({theme:r})=>r("colors"),ringOffsetWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},ringOpacity:({theme:r})=>({DEFAULT:"0.5",...r("opacity")}),ringWidth:{DEFAULT:"3px",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},rotate:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg",45:"45deg",90:"90deg",180:"180deg"},saturate:{0:"0",50:".5",100:"1",150:"1.5",200:"2"},scale:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5"},screens:{sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},scrollMargin:({theme:r})=>({...r("spacing")}),scrollPadding:({theme:r})=>r("spacing"),sepia:{0:"0",DEFAULT:"100%"},skew:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg"},space:({theme:r})=>({...r("spacing")}),spacing:{px:"1px",0:"0px",.5:"0.125rem",1:"0.25rem",1.5:"0.375rem",2:"0.5rem",2.5:"0.625rem",3:"0.75rem",3.5:"0.875rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem",11:"2.75rem",12:"3rem",14:"3.5rem",16:"4rem",20:"5rem",24:"6rem",28:"7rem",32:"8rem",36:"9rem",40:"10rem",44:"11rem",48:"12rem",52:"13rem",56:"14rem",60:"15rem",64:"16rem",72:"18rem",80:"20rem",96:"24rem"},stroke:({theme:r})=>({none:"none",...r("colors")}),strokeWidth:{0:"0",1:"1",2:"2"},supports:{},data:{},textColor:({theme:r})=>r("colors"),textDecorationColor:({theme:r})=>r("colors"),textDecorationThickness:{auto:"auto","from-font":"from-font",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},textIndent:({theme:r})=>({...r("spacing")}),textOpacity:({theme:r})=>r("opacity"),textUnderlineOffset:{auto:"auto",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},transformOrigin:{center:"center",top:"top","top-right":"top right",right:"right","bottom-right":"bottom right",bottom:"bottom","bottom-left":"bottom left",left:"left","top-left":"top left"},transitionDelay:{0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionDuration:{DEFAULT:"150ms",0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionProperty:{none:"none",all:"all",DEFAULT:"color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter",colors:"color, background-color, border-color, text-decoration-color, fill, stroke",opacity:"opacity",shadow:"box-shadow",transform:"transform"},transitionTimingFunction:{DEFAULT:"cubic-bezier(0.4, 0, 0.2, 1)",linear:"linear",in:"cubic-bezier(0.4, 0, 1, 1)",out:"cubic-bezier(0, 0, 0.2, 1)","in-out":"cubic-bezier(0.4, 0, 0.2, 1)"},translate:({theme:r})=>({...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),size:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),width:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",screen:"100vw",svw:"100svw",lvw:"100lvw",dvw:"100dvw",min:"min-content",max:"max-content",fit:"fit-content"}),willChange:{auto:"auto",scroll:"scroll-position",contents:"contents",transform:"transform"},zIndex:{auto:"auto",0:"0",10:"10",20:"20",30:"30",40:"40",50:"50"}},plugins:[]}});function nn(r){let e=(r?.presets??[ic.default]).slice().reverse().flatMap(n=>nn(n instanceof Function?n():n)),t={respectDefaultRingColorOpacity:{theme:{ringColor:({theme:n})=>({DEFAULT:"#3b82f67f",...n("colors")})}},disableColorOpacityUtilitiesByDefault:{corePlugins:{backgroundOpacity:!1,borderOpacity:!1,divideOpacity:!1,placeholderOpacity:!1,ringOpacity:!1,textOpacity:!1}}},i=Object.keys(t).filter(n=>we(r,n)).map(n=>t[n]);return[r,...i,...e]}var ic,nc=P(()=>{u();ic=pe(rn());ct()});var sc={};Ge(sc,{default:()=>zr});function zr(...r){let[,...e]=nn(r[0]);return na([...r,...e])}var sa=P(()=>{u();tc();nc()});var Ur={};Ge(Ur,{default:()=>me});var me,et=P(()=>{u();me={resolve:r=>r,extname:r=>"."+r.split(".").pop()}});function sn(r){return typeof r=="object"&&r!==null}function bx(r){return Object.keys(r).length===0}function ac(r){return typeof r=="string"||r instanceof String}function aa(r){return sn(r)&&r.config===void 0&&!bx(r)?null:sn(r)&&r.config!==void 0&&ac(r.config)?me.resolve(r.config):sn(r)&&r.config!==void 0&&sn(r.config)?null:ac(r)?me.resolve(r):wx()}function wx(){for(let r of yx)try{let e=me.resolve(r);return be.accessSync(e),e}catch(e){}return null}var yx,oc=P(()=>{u();ft();et();yx=["./tailwind.config.js","./tailwind.config.cjs","./tailwind.config.mjs","./tailwind.config.ts","./tailwind.config.cts","./tailwind.config.mts"]});var lc={};Ge(lc,{default:()=>oa});var oa,la=P(()=>{u();oa={parse:r=>({href:r})}});var ua=x(()=>{u()});var an=x((v3,cc)=>{u();"use strict";var uc=(Qi(),Af),fc=ua(),Jt=class extends Error{constructor(e,t,i,n,s,a){super(e);this.name="CssSyntaxError",this.reason=e,s&&(this.file=s),n&&(this.source=n),a&&(this.plugin=a),typeof t!="undefined"&&typeof i!="undefined"&&(typeof t=="number"?(this.line=t,this.column=i):(this.line=t.line,this.column=t.column,this.endLine=i.line,this.endColumn=i.column)),this.setMessage(),Error.captureStackTrace&&Error.captureStackTrace(this,Jt)}setMessage(){this.message=this.plugin?this.plugin+": ":"",this.message+=this.file?this.file:"",typeof this.line!="undefined"&&(this.message+=":"+this.line+":"+this.column),this.message+=": "+this.reason}showSourceCode(e){if(!this.source)return"";let t=this.source;e==null&&(e=uc.isColorSupported);let i=f=>f,n=f=>f,s=f=>f;if(e){let{bold:f,gray:d,red:p}=uc.createColors(!0);n=h=>f(p(h)),i=h=>d(h),fc&&(s=h=>fc(h))}let a=t.split(/\r?\n/),o=Math.max(this.line-3,0),l=Math.min(this.line+2,a.length),c=String(l).length;return a.slice(o,l).map((f,d)=>{let p=o+1+d,h=" "+(" "+p).slice(-c)+" | ";if(p===this.line){if(f.length>160){let v=20,y=Math.max(0,this.column-v),w=Math.max(this.column+v,this.endColumn+v),k=f.slice(y,w),S=i(h.replace(/\d/g," "))+f.slice(0,Math.min(this.column-1,v-1)).replace(/[^\t]/g," ");return n(">")+i(h)+s(k)+` + `+S+n("^")}let b=i(h.replace(/\d/g," "))+f.slice(0,this.column-1).replace(/[^\t]/g," ");return n(">")+i(h)+s(f)+` + `+b+n("^")}return" "+i(h)+s(f)}).join(` +`)}toString(){let e=this.showSourceCode();return e&&(e=` + +`+e+` +`),this.name+": "+this.message+e}};cc.exports=Jt;Jt.default=Jt});var fa=x((x3,dc)=>{u();"use strict";var pc={after:` +`,beforeClose:` +`,beforeComment:` +`,beforeDecl:` +`,beforeOpen:" ",beforeRule:` +`,colon:": ",commentLeft:" ",commentRight:" ",emptyBody:"",indent:" ",semicolon:!1};function vx(r){return r[0].toUpperCase()+r.slice(1)}var on=class{constructor(e){this.builder=e}atrule(e,t){let i="@"+e.name,n=e.params?this.rawValue(e,"params"):"";if(typeof e.raws.afterName!="undefined"?i+=e.raws.afterName:n&&(i+=" "),e.nodes)this.block(e,i+n);else{let s=(e.raws.between||"")+(t?";":"");this.builder(i+n+s,e)}}beforeAfter(e,t){let i;e.type==="decl"?i=this.raw(e,null,"beforeDecl"):e.type==="comment"?i=this.raw(e,null,"beforeComment"):t==="before"?i=this.raw(e,null,"beforeRule"):i=this.raw(e,null,"beforeClose");let n=e.parent,s=0;for(;n&&n.type!=="root";)s+=1,n=n.parent;if(i.includes(` +`)){let a=this.raw(e,null,"indent");if(a.length)for(let o=0;o0&&e.nodes[t].type==="comment";)t-=1;let i=this.raw(e,"semicolon");for(let n=0;n{if(n=l.raws[t],typeof n!="undefined")return!1})}return typeof n=="undefined"&&(n=pc[i]),a.rawCache[i]=n,n}rawBeforeClose(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length>0&&typeof i.raws.after!="undefined")return t=i.raws.after,t.includes(` +`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawBeforeComment(e,t){let i;return e.walkComments(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` +`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeDecl"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeDecl(e,t){let i;return e.walkDecls(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` +`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeRule"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeOpen(e){let t;return e.walk(i=>{if(i.type!=="decl"&&(t=i.raws.between,typeof t!="undefined"))return!1}),t}rawBeforeRule(e){let t;return e.walk(i=>{if(i.nodes&&(i.parent!==e||e.first!==i)&&typeof i.raws.before!="undefined")return t=i.raws.before,t.includes(` +`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawColon(e){let t;return e.walkDecls(i=>{if(typeof i.raws.between!="undefined")return t=i.raws.between.replace(/[^\s:]/g,""),!1}),t}rawEmptyBody(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length===0&&(t=i.raws.after,typeof t!="undefined"))return!1}),t}rawIndent(e){if(e.raws.indent)return e.raws.indent;let t;return e.walk(i=>{let n=i.parent;if(n&&n!==e&&n.parent&&n.parent===e&&typeof i.raws.before!="undefined"){let s=i.raws.before.split(` +`);return t=s[s.length-1],t=t.replace(/\S/g,""),!1}}),t}rawSemicolon(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length&&i.last.type==="decl"&&(t=i.raws.semicolon,typeof t!="undefined"))return!1}),t}rawValue(e,t){let i=e[t],n=e.raws[t];return n&&n.value===i?n.raw:i}root(e){this.body(e),e.raws.after&&this.builder(e.raws.after)}rule(e){this.block(e,this.rawValue(e,"selector")),e.raws.ownSemicolon&&this.builder(e.raws.ownSemicolon,e,"end")}stringify(e,t){if(!this[e.type])throw new Error("Unknown AST node type "+e.type+". Maybe you need to change PostCSS stringifier.");this[e.type](e,t)}};dc.exports=on;on.default=on});var Vr=x((k3,hc)=>{u();"use strict";var xx=fa();function ca(r,e){new xx(e).stringify(r)}hc.exports=ca;ca.default=ca});var ln=x((S3,pa)=>{u();"use strict";pa.exports.isClean=Symbol("isClean");pa.exports.my=Symbol("my")});var Gr=x((A3,mc)=>{u();"use strict";var kx=an(),Sx=fa(),Ax=Vr(),{isClean:Hr,my:Cx}=ln();function da(r,e){let t=new r.constructor;for(let i in r){if(!Object.prototype.hasOwnProperty.call(r,i)||i==="proxyCache")continue;let n=r[i],s=typeof n;i==="parent"&&s==="object"?e&&(t[i]=e):i==="source"?t[i]=n:Array.isArray(n)?t[i]=n.map(a=>da(a,t)):(s==="object"&&n!==null&&(n=da(n)),t[i]=n)}return t}function Wr(r,e){if(e&&typeof e.offset!="undefined")return e.offset;let t=1,i=1,n=0;for(let s=0;se.root().toProxy():e[t]},set(e,t,i){return e[t]===i||(e[t]=i,(t==="prop"||t==="value"||t==="name"||t==="params"||t==="important"||t==="text")&&e.markDirty()),!0}}}markClean(){this[Hr]=!0}markDirty(){if(this[Hr]){this[Hr]=!1;let e=this;for(;e=e.parent;)e[Hr]=!1}}next(){if(!this.parent)return;let e=this.parent.index(this);return this.parent.nodes[e+1]}positionBy(e){let t=this.source.start;if(e.index)t=this.positionInside(e.index);else if(e.word){let n=this.source.input.css.slice(Wr(this.source.input.css,this.source.start),Wr(this.source.input.css,this.source.end)).indexOf(e.word);n!==-1&&(t=this.positionInside(n))}return t}positionInside(e){let t=this.source.start.column,i=this.source.start.line,n=Wr(this.source.input.css,this.source.start),s=n+e;for(let a=n;atypeof l=="object"&&l.toJSON?l.toJSON(null,t):l);else if(typeof o=="object"&&o.toJSON)i[a]=o.toJSON(null,t);else if(a==="source"){let l=t.get(o.input);l==null&&(l=s,t.set(o.input,s),s++),i[a]={end:o.end,inputId:l,start:o.start}}else i[a]=o}return n&&(i.inputs=[...t.keys()].map(a=>a.toJSON())),i}toProxy(){return this.proxyCache||(this.proxyCache=new Proxy(this,this.getProxyProcessor())),this.proxyCache}toString(e=Ax){e.stringify&&(e=e.stringify);let t="";return e(this,i=>{t+=i}),t}warn(e,t,i){let n={node:this};for(let s in i)n[s]=i[s];return e.warn(t,n)}get proxyOf(){return this}};mc.exports=un;un.default=un});var Qr=x((C3,gc)=>{u();"use strict";var _x=Gr(),fn=class extends _x{constructor(e){super(e);this.type="comment"}};gc.exports=fn;fn.default=fn});var Yr=x((_3,yc)=>{u();"use strict";var Ex=Gr(),cn=class extends Ex{constructor(e){e&&typeof e.value!="undefined"&&typeof e.value!="string"&&(e={...e,value:String(e.value)});super(e);this.type="decl"}get variable(){return this.prop.startsWith("--")||this.prop[0]==="$"}};yc.exports=cn;cn.default=cn});var Et=x((E3,_c)=>{u();"use strict";var bc=Qr(),wc=Yr(),Ox=Gr(),{isClean:vc,my:xc}=ln(),ha,kc,Sc,ma;function Ac(r){return r.map(e=>(e.nodes&&(e.nodes=Ac(e.nodes)),delete e.source,e))}function Cc(r){if(r[vc]=!1,r.proxyOf.nodes)for(let e of r.proxyOf.nodes)Cc(e)}var Fe=class extends Ox{append(...e){for(let t of e){let i=this.normalize(t,this.last);for(let n of i)this.proxyOf.nodes.push(n)}return this.markDirty(),this}cleanRaws(e){if(super.cleanRaws(e),this.nodes)for(let t of this.nodes)t.cleanRaws(e)}each(e){if(!this.proxyOf.nodes)return;let t=this.getIterator(),i,n;for(;this.indexes[t]e[t](...i.map(n=>typeof n=="function"?(s,a)=>n(s.toProxy(),a):n)):t==="every"||t==="some"?i=>e[t]((n,...s)=>i(n.toProxy(),...s)):t==="root"?()=>e.root().toProxy():t==="nodes"?e.nodes.map(i=>i.toProxy()):t==="first"||t==="last"?e[t].toProxy():e[t]:e[t]},set(e,t,i){return e[t]===i||(e[t]=i,(t==="name"||t==="params"||t==="selector")&&e.markDirty()),!0}}}index(e){return typeof e=="number"?e:(e.proxyOf&&(e=e.proxyOf),this.proxyOf.nodes.indexOf(e))}insertAfter(e,t){let i=this.index(e),n=this.normalize(t,this.proxyOf.nodes[i]).reverse();i=this.index(e);for(let a of n)this.proxyOf.nodes.splice(i+1,0,a);let s;for(let a in this.indexes)s=this.indexes[a],i(n[xc]||Fe.rebuild(n),n=n.proxyOf,n.parent&&n.parent.removeChild(n),n[vc]&&Cc(n),n.raws||(n.raws={}),typeof n.raws.before=="undefined"&&t&&typeof t.raws.before!="undefined"&&(n.raws.before=t.raws.before.replace(/\S/g,"")),n.parent=this.proxyOf,n))}prepend(...e){e=e.reverse();for(let t of e){let i=this.normalize(t,this.first,"prepend").reverse();for(let n of i)this.proxyOf.nodes.unshift(n);for(let n in this.indexes)this.indexes[n]=this.indexes[n]+i.length}return this.markDirty(),this}push(e){return e.parent=this,this.proxyOf.nodes.push(e),this}removeAll(){for(let e of this.proxyOf.nodes)e.parent=void 0;return this.proxyOf.nodes=[],this.markDirty(),this}removeChild(e){e=this.index(e),this.proxyOf.nodes[e].parent=void 0,this.proxyOf.nodes.splice(e,1);let t;for(let i in this.indexes)t=this.indexes[i],t>=e&&(this.indexes[i]=t-1);return this.markDirty(),this}replaceValues(e,t,i){return i||(i=t,t={}),this.walkDecls(n=>{t.props&&!t.props.includes(n.prop)||t.fast&&!n.value.includes(t.fast)||(n.value=n.value.replace(e,i))}),this.markDirty(),this}some(e){return this.nodes.some(e)}walk(e){return this.each((t,i)=>{let n;try{n=e(t,i)}catch(s){throw t.addToError(s)}return n!==!1&&t.walk&&(n=t.walk(e)),n})}walkAtRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="atrule"&&e.test(i.name))return t(i,n)}):this.walk((i,n)=>{if(i.type==="atrule"&&i.name===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="atrule")return t(i,n)}))}walkComments(e){return this.walk((t,i)=>{if(t.type==="comment")return e(t,i)})}walkDecls(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="decl"&&e.test(i.prop))return t(i,n)}):this.walk((i,n)=>{if(i.type==="decl"&&i.prop===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="decl")return t(i,n)}))}walkRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="rule"&&e.test(i.selector))return t(i,n)}):this.walk((i,n)=>{if(i.type==="rule"&&i.selector===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="rule")return t(i,n)}))}get first(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[0]}get last(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[this.proxyOf.nodes.length-1]}};Fe.registerParse=r=>{kc=r};Fe.registerRule=r=>{ma=r};Fe.registerAtRule=r=>{ha=r};Fe.registerRoot=r=>{Sc=r};_c.exports=Fe;Fe.default=Fe;Fe.rebuild=r=>{r.type==="atrule"?Object.setPrototypeOf(r,ha.prototype):r.type==="rule"?Object.setPrototypeOf(r,ma.prototype):r.type==="decl"?Object.setPrototypeOf(r,wc.prototype):r.type==="comment"?Object.setPrototypeOf(r,bc.prototype):r.type==="root"&&Object.setPrototypeOf(r,Sc.prototype),r[xc]=!0,r.nodes&&r.nodes.forEach(e=>{Fe.rebuild(e)})}});var pn=x((O3,Oc)=>{u();"use strict";var Ec=Et(),Kr=class extends Ec{constructor(e){super(e);this.type="atrule"}append(...e){return this.proxyOf.nodes||(this.nodes=[]),super.append(...e)}prepend(...e){return this.proxyOf.nodes||(this.nodes=[]),super.prepend(...e)}};Oc.exports=Kr;Kr.default=Kr;Ec.registerAtRule(Kr)});var dn=x((T3,Pc)=>{u();"use strict";var Tx=Et(),Tc,Rc,er=class extends Tx{constructor(e){super({type:"document",...e});this.nodes||(this.nodes=[])}toResult(e={}){return new Tc(new Rc,this,e).stringify()}};er.registerLazyResult=r=>{Tc=r};er.registerProcessor=r=>{Rc=r};Pc.exports=er;er.default=er});var Dc=x((R3,Ic)=>{u();var Rx="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict",Px=(r,e=21)=>(t=e)=>{let i="",n=t;for(;n--;)i+=r[Math.random()*r.length|0];return i},Ix=(r=21)=>{let e="",t=r;for(;t--;)e+=Rx[Math.random()*64|0];return e};Ic.exports={nanoid:Ix,customAlphabet:Px}});var qc=x(()=>{u()});var ga=x((D3,$c)=>{u();$c.exports={}});var mn=x((q3,Bc)=>{u();"use strict";var{nanoid:Dx}=Dc(),{isAbsolute:ya,resolve:ba}=(et(),Ur),{SourceMapConsumer:qx,SourceMapGenerator:$x}=qc(),{fileURLToPath:Lc,pathToFileURL:hn}=(la(),lc),Mc=an(),Lx=ga(),wa=ua(),va=Symbol("fromOffsetCache"),Mx=Boolean(qx&&$x),Nc=Boolean(ba&&ya),Xr=class{constructor(e,t={}){if(e===null||typeof e=="undefined"||typeof e=="object"&&!e.toString)throw new Error(`PostCSS received ${e} instead of CSS string`);if(this.css=e.toString(),this.css[0]==="\uFEFF"||this.css[0]==="\uFFFE"?(this.hasBOM=!0,this.css=this.css.slice(1)):this.hasBOM=!1,t.from&&(!Nc||/^\w+:\/\//.test(t.from)||ya(t.from)?this.file=t.from:this.file=ba(t.from)),Nc&&Mx){let i=new Lx(this.css,t);if(i.text){this.map=i;let n=i.consumer().file;!this.file&&n&&(this.file=this.mapResolve(n))}}this.file||(this.id=""),this.map&&(this.map.file=this.from)}error(e,t,i,n={}){let s,a,o;if(t&&typeof t=="object"){let c=t,f=i;if(typeof c.offset=="number"){let d=this.fromOffset(c.offset);t=d.line,i=d.col}else t=c.line,i=c.column;if(typeof f.offset=="number"){let d=this.fromOffset(f.offset);a=d.line,s=d.col}else a=f.line,s=f.column}else if(!i){let c=this.fromOffset(t);t=c.line,i=c.col}let l=this.origin(t,i,a,s);return l?o=new Mc(e,l.endLine===void 0?l.line:{column:l.column,line:l.line},l.endLine===void 0?l.column:{column:l.endColumn,line:l.endLine},l.source,l.file,n.plugin):o=new Mc(e,a===void 0?t:{column:i,line:t},a===void 0?i:{column:s,line:a},this.css,this.file,n.plugin),o.input={column:i,endColumn:s,endLine:a,line:t,source:this.css},this.file&&(hn&&(o.input.url=hn(this.file).toString()),o.input.file=this.file),o}fromOffset(e){let t,i;if(this[va])i=this[va];else{let s=this.css.split(` +`);i=new Array(s.length);let a=0;for(let o=0,l=s.length;o=t)n=i.length-1;else{let s=i.length-2,a;for(;n>1),e=i[a+1])n=a+1;else{n=a;break}}return{col:e-i[n]+1,line:n+1}}mapResolve(e){return/^\w+:\/\//.test(e)?e:ba(this.map.consumer().sourceRoot||this.map.root||".",e)}origin(e,t,i,n){if(!this.map)return!1;let s=this.map.consumer(),a=s.originalPositionFor({column:t,line:e});if(!a.source)return!1;let o;typeof i=="number"&&(o=s.originalPositionFor({column:n,line:i}));let l;ya(a.source)?l=hn(a.source):l=new URL(a.source,this.map.consumer().sourceRoot||hn(this.map.mapFile));let c={column:a.column,endColumn:o&&o.column,endLine:o&&o.line,line:a.line,url:l.toString()};if(l.protocol==="file:")if(Lc)c.file=Lc(l);else throw new Error("file: protocol is not available in this PostCSS build");let f=s.sourceContentFor(a.source);return f&&(c.source=f),c}toJSON(){let e={};for(let t of["hasBOM","css","file","id"])this[t]!=null&&(e[t]=this[t]);return this.map&&(e.map={...this.map},e.map.consumerCache&&(e.map.consumerCache=void 0)),e}get from(){return this.file||this.id}};Bc.exports=Xr;Xr.default=Xr;wa&&wa.registerInput&&wa.registerInput(Xr)});var tr=x(($3,Uc)=>{u();"use strict";var Fc=Et(),jc,zc,Ut=class extends Fc{constructor(e){super(e);this.type="root",this.nodes||(this.nodes=[])}normalize(e,t,i){let n=super.normalize(e);if(t){if(i==="prepend")this.nodes.length>1?t.raws.before=this.nodes[1].raws.before:delete t.raws.before;else if(this.first!==t)for(let s of n)s.raws.before=t.raws.before}return n}removeChild(e,t){let i=this.index(e);return!t&&i===0&&this.nodes.length>1&&(this.nodes[1].raws.before=this.nodes[i].raws.before),super.removeChild(e)}toResult(e={}){return new jc(new zc,this,e).stringify()}};Ut.registerLazyResult=r=>{jc=r};Ut.registerProcessor=r=>{zc=r};Uc.exports=Ut;Ut.default=Ut;Fc.registerRoot(Ut)});var xa=x((L3,Vc)=>{u();"use strict";var Zr={comma(r){return Zr.split(r,[","],!0)},space(r){let e=[" ",` +`," "];return Zr.split(r,e)},split(r,e,t){let i=[],n="",s=!1,a=0,o=!1,l="",c=!1;for(let f of r)c?c=!1:f==="\\"?c=!0:o?f===l&&(o=!1):f==='"'||f==="'"?(o=!0,l=f):f==="("?a+=1:f===")"?a>0&&(a-=1):a===0&&e.includes(f)&&(s=!0),s?(n!==""&&i.push(n.trim()),n="",s=!1):n+=f;return(t||n!=="")&&i.push(n.trim()),i}};Vc.exports=Zr;Zr.default=Zr});var gn=x((M3,Wc)=>{u();"use strict";var Hc=Et(),Nx=xa(),Jr=class extends Hc{constructor(e){super(e);this.type="rule",this.nodes||(this.nodes=[])}get selectors(){return Nx.comma(this.selector)}set selectors(e){let t=this.selector?this.selector.match(/,\s*/):null,i=t?t[0]:","+this.raw("between","beforeOpen");this.selector=e.join(i)}};Wc.exports=Jr;Jr.default=Jr;Hc.registerRule(Jr)});var Qc=x((N3,Gc)=>{u();"use strict";var Bx=pn(),Fx=Qr(),jx=Yr(),zx=mn(),Ux=ga(),Vx=tr(),Hx=gn();function ei(r,e){if(Array.isArray(r))return r.map(n=>ei(n));let{inputs:t,...i}=r;if(t){e=[];for(let n of t){let s={...n,__proto__:zx.prototype};s.map&&(s.map={...s.map,__proto__:Ux.prototype}),e.push(s)}}if(i.nodes&&(i.nodes=r.nodes.map(n=>ei(n,e))),i.source){let{inputId:n,...s}=i.source;i.source=s,n!=null&&(i.source.input=e[n])}if(i.type==="root")return new Vx(i);if(i.type==="decl")return new jx(i);if(i.type==="rule")return new Hx(i);if(i.type==="comment")return new Fx(i);if(i.type==="atrule")return new Bx(i);throw new Error("Unknown node type: "+r.type)}Gc.exports=ei;ei.default=ei});var ka=x((B3,Yc)=>{u();Yc.exports=function(r,e){return{generate:()=>{let t="";return r(e,i=>{t+=i}),[t]}}}});var ep=x((F3,Jc)=>{u();"use strict";var Sa="'".charCodeAt(0),Kc='"'.charCodeAt(0),yn="\\".charCodeAt(0),Xc="/".charCodeAt(0),bn=` +`.charCodeAt(0),ti=" ".charCodeAt(0),wn="\f".charCodeAt(0),vn=" ".charCodeAt(0),xn="\r".charCodeAt(0),Wx="[".charCodeAt(0),Gx="]".charCodeAt(0),Qx="(".charCodeAt(0),Yx=")".charCodeAt(0),Kx="{".charCodeAt(0),Xx="}".charCodeAt(0),Zx=";".charCodeAt(0),Jx="*".charCodeAt(0),e1=":".charCodeAt(0),t1="@".charCodeAt(0),kn=/[\t\n\f\r "#'()/;[\\\]{}]/g,Sn=/[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g,r1=/.[\r\n"'(/\\]/,Zc=/[\da-f]/i;Jc.exports=function(e,t={}){let i=e.css.valueOf(),n=t.ignoreErrors,s,a,o,l,c,f,d,p,h,b,v=i.length,y=0,w=[],k=[];function S(){return y}function E(R){throw e.error("Unclosed "+R,y)}function T(){return k.length===0&&y>=v}function B(R){if(k.length)return k.pop();if(y>=v)return;let F=R?R.ignoreUnclosed:!1;switch(s=i.charCodeAt(y),s){case bn:case ti:case vn:case xn:case wn:{l=y;do l+=1,s=i.charCodeAt(l);while(s===ti||s===bn||s===vn||s===xn||s===wn);f=["space",i.slice(y,l)],y=l-1;break}case Wx:case Gx:case Kx:case Xx:case e1:case Zx:case Yx:{let Y=String.fromCharCode(s);f=[Y,Y,y];break}case Qx:{if(b=w.length?w.pop()[1]:"",h=i.charCodeAt(y+1),b==="url"&&h!==Sa&&h!==Kc&&h!==ti&&h!==bn&&h!==vn&&h!==wn&&h!==xn){l=y;do{if(d=!1,l=i.indexOf(")",l+1),l===-1)if(n||F){l=y;break}else E("bracket");for(p=l;i.charCodeAt(p-1)===yn;)p-=1,d=!d}while(d);f=["brackets",i.slice(y,l+1),y,l],y=l}else l=i.indexOf(")",y+1),a=i.slice(y,l+1),l===-1||r1.test(a)?f=["(","(",y]:(f=["brackets",a,y,l],y=l);break}case Sa:case Kc:{c=s===Sa?"'":'"',l=y;do{if(d=!1,l=i.indexOf(c,l+1),l===-1)if(n||F){l=y+1;break}else E("string");for(p=l;i.charCodeAt(p-1)===yn;)p-=1,d=!d}while(d);f=["string",i.slice(y,l+1),y,l],y=l;break}case t1:{kn.lastIndex=y+1,kn.test(i),kn.lastIndex===0?l=i.length-1:l=kn.lastIndex-2,f=["at-word",i.slice(y,l+1),y,l],y=l;break}case yn:{for(l=y,o=!0;i.charCodeAt(l+1)===yn;)l+=1,o=!o;if(s=i.charCodeAt(l+1),o&&s!==Xc&&s!==ti&&s!==bn&&s!==vn&&s!==xn&&s!==wn&&(l+=1,Zc.test(i.charAt(l)))){for(;Zc.test(i.charAt(l+1));)l+=1;i.charCodeAt(l+1)===ti&&(l+=1)}f=["word",i.slice(y,l+1),y,l],y=l;break}default:{s===Xc&&i.charCodeAt(y+1)===Jx?(l=i.indexOf("*/",y+2)+1,l===0&&(n||F?l=i.length:E("comment")),f=["comment",i.slice(y,l+1),y,l],y=l):(Sn.lastIndex=y+1,Sn.test(i),Sn.lastIndex===0?l=i.length-1:l=Sn.lastIndex-2,f=["word",i.slice(y,l+1),y,l],w.push(f),y=l);break}}return y++,f}function N(R){k.push(R)}return{back:N,endOfFile:T,nextToken:B,position:S}}});var sp=x((j3,np)=>{u();"use strict";var i1=pn(),n1=Qr(),s1=Yr(),a1=tr(),tp=gn(),o1=ep(),rp={empty:!0,space:!0};function l1(r){for(let e=r.length-1;e>=0;e--){let t=r[e],i=t[3]||t[2];if(i)return i}}var ip=class{constructor(e){this.input=e,this.root=new a1,this.current=this.root,this.spaces="",this.semicolon=!1,this.createTokenizer(),this.root.source={input:e,start:{column:1,line:1,offset:0}}}atrule(e){let t=new i1;t.name=e[1].slice(1),t.name===""&&this.unnamedAtrule(t,e),this.init(t,e[2]);let i,n,s,a=!1,o=!1,l=[],c=[];for(;!this.tokenizer.endOfFile();){if(e=this.tokenizer.nextToken(),i=e[0],i==="("||i==="["?c.push(i==="("?")":"]"):i==="{"&&c.length>0?c.push("}"):i===c[c.length-1]&&c.pop(),c.length===0)if(i===";"){t.source.end=this.getPosition(e[2]),t.source.end.offset++,this.semicolon=!0;break}else if(i==="{"){o=!0;break}else if(i==="}"){if(l.length>0){for(s=l.length-1,n=l[s];n&&n[0]==="space";)n=l[--s];n&&(t.source.end=this.getPosition(n[3]||n[2]),t.source.end.offset++)}this.end(e);break}else l.push(e);else l.push(e);if(this.tokenizer.endOfFile()){a=!0;break}}t.raws.between=this.spacesAndCommentsFromEnd(l),l.length?(t.raws.afterName=this.spacesAndCommentsFromStart(l),this.raw(t,"params",l),a&&(e=l[l.length-1],t.source.end=this.getPosition(e[3]||e[2]),t.source.end.offset++,this.spaces=t.raws.between,t.raws.between="")):(t.raws.afterName="",t.params=""),o&&(t.nodes=[],this.current=t)}checkMissedSemicolon(e){let t=this.colon(e);if(t===!1)return;let i=0,n;for(let s=t-1;s>=0&&(n=e[s],!(n[0]!=="space"&&(i+=1,i===2)));s--);throw this.input.error("Missed semicolon",n[0]==="word"?n[3]+1:n[2])}colon(e){let t=0,i,n,s;for(let[a,o]of e.entries()){if(n=o,s=n[0],s==="("&&(t+=1),s===")"&&(t-=1),t===0&&s===":")if(!i)this.doubleColon(n);else{if(i[0]==="word"&&i[1]==="progid")continue;return a}i=n}return!1}comment(e){let t=new n1;this.init(t,e[2]),t.source.end=this.getPosition(e[3]||e[2]),t.source.end.offset++;let i=e[1].slice(2,-2);if(/^\s*$/.test(i))t.text="",t.raws.left=i,t.raws.right="";else{let n=i.match(/^(\s*)([^]*\S)(\s*)$/);t.text=n[2],t.raws.left=n[1],t.raws.right=n[3]}}createTokenizer(){this.tokenizer=o1(this.input)}decl(e,t){let i=new s1;this.init(i,e[0][2]);let n=e[e.length-1];for(n[0]===";"&&(this.semicolon=!0,e.pop()),i.source.end=this.getPosition(n[3]||n[2]||l1(e)),i.source.end.offset++;e[0][0]!=="word";)e.length===1&&this.unknownWord(e),i.raws.before+=e.shift()[1];for(i.source.start=this.getPosition(e[0][2]),i.prop="";e.length;){let c=e[0][0];if(c===":"||c==="space"||c==="comment")break;i.prop+=e.shift()[1]}i.raws.between="";let s;for(;e.length;)if(s=e.shift(),s[0]===":"){i.raws.between+=s[1];break}else s[0]==="word"&&/\w/.test(s[1])&&this.unknownWord([s]),i.raws.between+=s[1];(i.prop[0]==="_"||i.prop[0]==="*")&&(i.raws.before+=i.prop[0],i.prop=i.prop.slice(1));let a=[],o;for(;e.length&&(o=e[0][0],!(o!=="space"&&o!=="comment"));)a.push(e.shift());this.precheckMissedSemicolon(e);for(let c=e.length-1;c>=0;c--){if(s=e[c],s[1].toLowerCase()==="!important"){i.important=!0;let f=this.stringFrom(e,c);f=this.spacesFromEnd(e)+f,f!==" !important"&&(i.raws.important=f);break}else if(s[1].toLowerCase()==="important"){let f=e.slice(0),d="";for(let p=c;p>0;p--){let h=f[p][0];if(d.trim().startsWith("!")&&h!=="space")break;d=f.pop()[1]+d}d.trim().startsWith("!")&&(i.important=!0,i.raws.important=d,e=f)}if(s[0]!=="space"&&s[0]!=="comment")break}e.some(c=>c[0]!=="space"&&c[0]!=="comment")&&(i.raws.between+=a.map(c=>c[1]).join(""),a=[]),this.raw(i,"value",a.concat(e),t),i.value.includes(":")&&!t&&this.checkMissedSemicolon(e)}doubleColon(e){throw this.input.error("Double colon",{offset:e[2]},{offset:e[2]+e[1].length})}emptyRule(e){let t=new tp;this.init(t,e[2]),t.selector="",t.raws.between="",this.current=t}end(e){this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.semicolon=!1,this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.spaces="",this.current.parent?(this.current.source.end=this.getPosition(e[2]),this.current.source.end.offset++,this.current=this.current.parent):this.unexpectedClose(e)}endFile(){this.current.parent&&this.unclosedBlock(),this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.root.source.end=this.getPosition(this.tokenizer.position())}freeSemicolon(e){if(this.spaces+=e[1],this.current.nodes){let t=this.current.nodes[this.current.nodes.length-1];t&&t.type==="rule"&&!t.raws.ownSemicolon&&(t.raws.ownSemicolon=this.spaces,this.spaces="")}}getPosition(e){let t=this.input.fromOffset(e);return{column:t.col,line:t.line,offset:e}}init(e,t){this.current.push(e),e.source={input:this.input,start:this.getPosition(t)},e.raws.before=this.spaces,this.spaces="",e.type!=="comment"&&(this.semicolon=!1)}other(e){let t=!1,i=null,n=!1,s=null,a=[],o=e[1].startsWith("--"),l=[],c=e;for(;c;){if(i=c[0],l.push(c),i==="("||i==="[")s||(s=c),a.push(i==="("?")":"]");else if(o&&n&&i==="{")s||(s=c),a.push("}");else if(a.length===0)if(i===";")if(n){this.decl(l,o);return}else break;else if(i==="{"){this.rule(l);return}else if(i==="}"){this.tokenizer.back(l.pop()),t=!0;break}else i===":"&&(n=!0);else i===a[a.length-1]&&(a.pop(),a.length===0&&(s=null));c=this.tokenizer.nextToken()}if(this.tokenizer.endOfFile()&&(t=!0),a.length>0&&this.unclosedBracket(s),t&&n){if(!o)for(;l.length&&(c=l[l.length-1][0],!(c!=="space"&&c!=="comment"));)this.tokenizer.back(l.pop());this.decl(l,o)}else this.unknownWord(l)}parse(){let e;for(;!this.tokenizer.endOfFile();)switch(e=this.tokenizer.nextToken(),e[0]){case"space":this.spaces+=e[1];break;case";":this.freeSemicolon(e);break;case"}":this.end(e);break;case"comment":this.comment(e);break;case"at-word":this.atrule(e);break;case"{":this.emptyRule(e);break;default:this.other(e);break}this.endFile()}precheckMissedSemicolon(){}raw(e,t,i,n){let s,a,o=i.length,l="",c=!0,f,d;for(let p=0;ph+b[1],"");e.raws[t]={raw:p,value:l}}e[t]=l}rule(e){e.pop();let t=new tp;this.init(t,e[0][2]),t.raws.between=this.spacesAndCommentsFromEnd(e),this.raw(t,"selector",e),this.current=t}spacesAndCommentsFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],!(t!=="space"&&t!=="comment"));)i=e.pop()[1]+i;return i}spacesAndCommentsFromStart(e){let t,i="";for(;e.length&&(t=e[0][0],!(t!=="space"&&t!=="comment"));)i+=e.shift()[1];return i}spacesFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],t==="space");)i=e.pop()[1]+i;return i}stringFrom(e,t){let i="";for(let n=t;n{u();"use strict";var u1=Et(),f1=mn(),c1=sp();function An(r,e){let t=new f1(r,e),i=new c1(t);try{i.parse()}catch(n){throw n}return i.root}ap.exports=An;An.default=An;u1.registerParse(An)});var Aa=x((U3,op)=>{u();"use strict";var _n=class{constructor(e,t={}){if(this.type="warning",this.text=e,t.node&&t.node.source){let i=t.node.rangeBy(t);this.line=i.start.line,this.column=i.start.column,this.endLine=i.end.line,this.endColumn=i.end.column}for(let i in t)this[i]=t[i]}toString(){return this.node?this.node.error(this.text,{index:this.index,plugin:this.plugin,word:this.word}).message:this.plugin?this.plugin+": "+this.text:this.text}};op.exports=_n;_n.default=_n});var On=x((V3,lp)=>{u();"use strict";var p1=Aa(),En=class{constructor(e,t,i){this.processor=e,this.messages=[],this.root=t,this.opts=i,this.css=void 0,this.map=void 0}toString(){return this.css}warn(e,t={}){t.plugin||this.lastPlugin&&this.lastPlugin.postcssPlugin&&(t.plugin=this.lastPlugin.postcssPlugin);let i=new p1(e,t);return this.messages.push(i),i}warnings(){return this.messages.filter(e=>e.type==="warning")}get content(){return this.css}};lp.exports=En;En.default=En});var Ca=x((H3,fp)=>{u();"use strict";var up={};fp.exports=function(e){up[e]||(up[e]=!0,typeof console!="undefined"&&console.warn&&console.warn(e))}});var Oa=x((G3,hp)=>{u();"use strict";var d1=Et(),h1=dn(),m1=ka(),g1=Cn(),cp=On(),y1=tr(),b1=Vr(),{isClean:tt,my:w1}=ln(),W3=Ca(),v1={atrule:"AtRule",comment:"Comment",decl:"Declaration",document:"Document",root:"Root",rule:"Rule"},x1={AtRule:!0,AtRuleExit:!0,Comment:!0,CommentExit:!0,Declaration:!0,DeclarationExit:!0,Document:!0,DocumentExit:!0,Once:!0,OnceExit:!0,postcssPlugin:!0,prepare:!0,Root:!0,RootExit:!0,Rule:!0,RuleExit:!0},k1={Once:!0,postcssPlugin:!0,prepare:!0},rr=0;function ri(r){return typeof r=="object"&&typeof r.then=="function"}function pp(r){let e=!1,t=v1[r.type];return r.type==="decl"?e=r.prop.toLowerCase():r.type==="atrule"&&(e=r.name.toLowerCase()),e&&r.append?[t,t+"-"+e,rr,t+"Exit",t+"Exit-"+e]:e?[t,t+"-"+e,t+"Exit",t+"Exit-"+e]:r.append?[t,rr,t+"Exit"]:[t,t+"Exit"]}function dp(r){let e;return r.type==="document"?e=["Document",rr,"DocumentExit"]:r.type==="root"?e=["Root",rr,"RootExit"]:e=pp(r),{eventIndex:0,events:e,iterator:0,node:r,visitorIndex:0,visitors:[]}}function _a(r){return r[tt]=!1,r.nodes&&r.nodes.forEach(e=>_a(e)),r}var Ea={},pt=class{constructor(e,t,i){this.stringified=!1,this.processed=!1;let n;if(typeof t=="object"&&t!==null&&(t.type==="root"||t.type==="document"))n=_a(t);else if(t instanceof pt||t instanceof cp)n=_a(t.root),t.map&&(typeof i.map=="undefined"&&(i.map={}),i.map.inline||(i.map.inline=!1),i.map.prev=t.map);else{let s=g1;i.syntax&&(s=i.syntax.parse),i.parser&&(s=i.parser),s.parse&&(s=s.parse);try{n=s(t,i)}catch(a){this.processed=!0,this.error=a}n&&!n[w1]&&d1.rebuild(n)}this.result=new cp(e,n,i),this.helpers={...Ea,postcss:Ea,result:this.result},this.plugins=this.processor.plugins.map(s=>typeof s=="object"&&s.prepare?{...s,...s.prepare(this.result)}:s)}async(){return this.error?Promise.reject(this.error):this.processed?Promise.resolve(this.result):(this.processing||(this.processing=this.runAsync()),this.processing)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}getAsyncError(){throw new Error("Use process(css).then(cb) to work with async plugins")}handleError(e,t){let i=this.result.lastPlugin;try{t&&t.addToError(e),this.error=e,e.name==="CssSyntaxError"&&!e.plugin?(e.plugin=i.postcssPlugin,e.setMessage()):i.postcssVersion}catch(n){console&&console.error&&console.error(n)}return e}prepareVisitors(){this.listeners={};let e=(t,i,n)=>{this.listeners[i]||(this.listeners[i]=[]),this.listeners[i].push([t,n])};for(let t of this.plugins)if(typeof t=="object")for(let i in t){if(!x1[i]&&/^[A-Z]/.test(i))throw new Error(`Unknown event ${i} in ${t.postcssPlugin}. Try to update PostCSS (${this.processor.version} now).`);if(!k1[i])if(typeof t[i]=="object")for(let n in t[i])n==="*"?e(t,i,t[i][n]):e(t,i+"-"+n.toLowerCase(),t[i][n]);else typeof t[i]=="function"&&e(t,i,t[i])}this.hasListener=Object.keys(this.listeners).length>0}async runAsync(){this.plugin=0;for(let e=0;e0;){let i=this.visitTick(t);if(ri(i))try{await i}catch(n){let s=t[t.length-1].node;throw this.handleError(n,s)}}}if(this.listeners.OnceExit)for(let[t,i]of this.listeners.OnceExit){this.result.lastPlugin=t;try{if(e.type==="document"){let n=e.nodes.map(s=>i(s,this.helpers));await Promise.all(n)}else await i(e,this.helpers)}catch(n){throw this.handleError(n)}}}return this.processed=!0,this.stringify()}runOnRoot(e){this.result.lastPlugin=e;try{if(typeof e=="object"&&e.Once){if(this.result.root.type==="document"){let t=this.result.root.nodes.map(i=>e.Once(i,this.helpers));return ri(t[0])?Promise.all(t):t}return e.Once(this.result.root,this.helpers)}else if(typeof e=="function")return e(this.result.root,this.result)}catch(t){throw this.handleError(t)}}stringify(){if(this.error)throw this.error;if(this.stringified)return this.result;this.stringified=!0,this.sync();let e=this.result.opts,t=b1;e.syntax&&(t=e.syntax.stringify),e.stringifier&&(t=e.stringifier),t.stringify&&(t=t.stringify);let n=new m1(t,this.result.root,this.result.opts).generate();return this.result.css=n[0],this.result.map=n[1],this.result}sync(){if(this.error)throw this.error;if(this.processed)return this.result;if(this.processed=!0,this.processing)throw this.getAsyncError();for(let e of this.plugins){let t=this.runOnRoot(e);if(ri(t))throw this.getAsyncError()}if(this.prepareVisitors(),this.hasListener){let e=this.result.root;for(;!e[tt];)e[tt]=!0,this.walkSync(e);if(this.listeners.OnceExit)if(e.type==="document")for(let t of e.nodes)this.visitSync(this.listeners.OnceExit,t);else this.visitSync(this.listeners.OnceExit,e)}return this.result}then(e,t){return this.async().then(e,t)}toString(){return this.css}visitSync(e,t){for(let[i,n]of e){this.result.lastPlugin=i;let s;try{s=n(t,this.helpers)}catch(a){throw this.handleError(a,t.proxyOf)}if(t.type!=="root"&&t.type!=="document"&&!t.parent)return!0;if(ri(s))throw this.getAsyncError()}}visitTick(e){let t=e[e.length-1],{node:i,visitors:n}=t;if(i.type!=="root"&&i.type!=="document"&&!i.parent){e.pop();return}if(n.length>0&&t.visitorIndex{n[tt]||this.walkSync(n)});else{let n=this.listeners[i];if(n&&this.visitSync(n,e.toProxy()))return}}warnings(){return this.sync().warnings()}get content(){return this.stringify().content}get css(){return this.stringify().css}get map(){return this.stringify().map}get messages(){return this.sync().messages}get opts(){return this.result.opts}get processor(){return this.result.processor}get root(){return this.sync().root}get[Symbol.toStringTag](){return"LazyResult"}};pt.registerPostcss=r=>{Ea=r};hp.exports=pt;pt.default=pt;y1.registerLazyResult(pt);h1.registerLazyResult(pt)});var gp=x((Y3,mp)=>{u();"use strict";var S1=ka(),A1=Cn(),C1=On(),_1=Vr(),Q3=Ca(),Tn=class{constructor(e,t,i){t=t.toString(),this.stringified=!1,this._processor=e,this._css=t,this._opts=i,this._map=void 0;let n,s=_1;this.result=new C1(this._processor,n,this._opts),this.result.css=t;let a=this;Object.defineProperty(this.result,"root",{get(){return a.root}});let o=new S1(s,n,this._opts,t);if(o.isMap()){let[l,c]=o.generate();l&&(this.result.css=l),c&&(this.result.map=c)}else o.clearAnnotation(),this.result.css=o.css}async(){return this.error?Promise.reject(this.error):Promise.resolve(this.result)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}sync(){if(this.error)throw this.error;return this.result}then(e,t){return this.async().then(e,t)}toString(){return this._css}warnings(){return[]}get content(){return this.result.css}get css(){return this.result.css}get map(){return this.result.map}get messages(){return[]}get opts(){return this.result.opts}get processor(){return this.result.processor}get root(){if(this._root)return this._root;let e,t=A1;try{e=t(this._css,this._opts)}catch(i){this.error=i}if(this.error)throw this.error;return this._root=e,e}get[Symbol.toStringTag](){return"NoWorkResult"}};mp.exports=Tn;Tn.default=Tn});var bp=x((K3,yp)=>{u();"use strict";var E1=dn(),O1=Oa(),T1=gp(),R1=tr(),ir=class{constructor(e=[]){this.version="8.4.49",this.plugins=this.normalize(e)}normalize(e){let t=[];for(let i of e)if(i.postcss===!0?i=i():i.postcss&&(i=i.postcss),typeof i=="object"&&Array.isArray(i.plugins))t=t.concat(i.plugins);else if(typeof i=="object"&&i.postcssPlugin)t.push(i);else if(typeof i=="function")t.push(i);else if(!(typeof i=="object"&&(i.parse||i.stringify)))throw new Error(i+" is not a PostCSS plugin");return t}process(e,t={}){return!this.plugins.length&&!t.parser&&!t.stringifier&&!t.syntax?new T1(this,e,t):new O1(this,e,t)}use(e){return this.plugins=this.plugins.concat(this.normalize([e])),this}};yp.exports=ir;ir.default=ir;R1.registerProcessor(ir);E1.registerProcessor(ir)});var $e=x((X3,Cp)=>{u();"use strict";var wp=pn(),vp=Qr(),P1=Et(),I1=an(),xp=Yr(),kp=dn(),D1=Qc(),q1=mn(),$1=Oa(),L1=xa(),M1=Gr(),N1=Cn(),Ta=bp(),B1=On(),Sp=tr(),Ap=gn(),F1=Vr(),j1=Aa();function J(...r){return r.length===1&&Array.isArray(r[0])&&(r=r[0]),new Ta(r)}J.plugin=function(e,t){let i=!1;function n(...a){console&&console.warn&&!i&&(i=!0,console.warn(e+`: postcss.plugin was deprecated. Migration guide: +https://evilmartians.com/chronicles/postcss-8-plugin-migration`),m.env.LANG&&m.env.LANG.startsWith("cn")&&console.warn(e+`: \u91CC\u9762 postcss.plugin \u88AB\u5F03\u7528. \u8FC1\u79FB\u6307\u5357: +https://www.w3ctech.com/topic/2226`));let o=t(...a);return o.postcssPlugin=e,o.postcssVersion=new Ta().version,o}let s;return Object.defineProperty(n,"postcss",{get(){return s||(s=n()),s}}),n.process=function(a,o,l){return J([n(l)]).process(a,o)},n};J.stringify=F1;J.parse=N1;J.fromJSON=D1;J.list=L1;J.comment=r=>new vp(r);J.atRule=r=>new wp(r);J.decl=r=>new xp(r);J.rule=r=>new Ap(r);J.root=r=>new Sp(r);J.document=r=>new kp(r);J.CssSyntaxError=I1;J.Declaration=xp;J.Container=P1;J.Processor=Ta;J.Document=kp;J.Comment=vp;J.Warning=j1;J.AtRule=wp;J.Result=B1;J.Input=q1;J.Rule=Ap;J.Root=Sp;J.Node=M1;$1.registerPostcss(J);Cp.exports=J;J.default=J});var re,ee,Z3,J3,eI,tI,rI,iI,nI,sI,aI,oI,lI,uI,fI,cI,pI,dI,hI,mI,gI,yI,bI,wI,vI,xI,Ot=P(()=>{u();re=pe($e()),ee=re.default,Z3=re.default.stringify,J3=re.default.fromJSON,eI=re.default.plugin,tI=re.default.parse,rI=re.default.list,iI=re.default.document,nI=re.default.comment,sI=re.default.atRule,aI=re.default.rule,oI=re.default.decl,lI=re.default.root,uI=re.default.CssSyntaxError,fI=re.default.Declaration,cI=re.default.Container,pI=re.default.Processor,dI=re.default.Document,hI=re.default.Comment,mI=re.default.Warning,gI=re.default.AtRule,yI=re.default.Result,bI=re.default.Input,wI=re.default.Rule,vI=re.default.Root,xI=re.default.Node});var Ra=x((SI,_p)=>{u();_p.exports=function(r,e,t,i,n){for(e=e.split?e.split("."):e,i=0;i{u();"use strict";Rn.__esModule=!0;Rn.default=V1;function z1(r){for(var e=r.toLowerCase(),t="",i=!1,n=0;n<6&&e[n]!==void 0;n++){var s=e.charCodeAt(n),a=s>=97&&s<=102||s>=48&&s<=57;if(i=s===32,!a)break;t+=e[n]}if(t.length!==0){var o=parseInt(t,16),l=o>=55296&&o<=57343;return l||o===0||o>1114111?["\uFFFD",t.length+(i?1:0)]:[String.fromCodePoint(o),t.length+(i?1:0)]}}var U1=/\\/;function V1(r){var e=U1.test(r);if(!e)return r;for(var t="",i=0;i{u();"use strict";In.__esModule=!0;In.default=H1;function H1(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();if(!r[n])return;r=r[n]}return r}Op.exports=In.default});var Pp=x((Dn,Rp)=>{u();"use strict";Dn.__esModule=!0;Dn.default=W1;function W1(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();r[n]||(r[n]={}),r=r[n]}}Rp.exports=Dn.default});var Dp=x((qn,Ip)=>{u();"use strict";qn.__esModule=!0;qn.default=G1;function G1(r){for(var e="",t=r.indexOf("/*"),i=0;t>=0;){e=e+r.slice(i,t);var n=r.indexOf("*/",t+2);if(n<0)return e;i=n+2,t=r.indexOf("/*",i)}return e=e+r.slice(i),e}Ip.exports=qn.default});var ii=x(rt=>{u();"use strict";rt.__esModule=!0;rt.unesc=rt.stripComments=rt.getProp=rt.ensureObject=void 0;var Q1=$n(Pn());rt.unesc=Q1.default;var Y1=$n(Tp());rt.getProp=Y1.default;var K1=$n(Pp());rt.ensureObject=K1.default;var X1=$n(Dp());rt.stripComments=X1.default;function $n(r){return r&&r.__esModule?r:{default:r}}});var dt=x((ni,Lp)=>{u();"use strict";ni.__esModule=!0;ni.default=void 0;var qp=ii();function $p(r,e){for(var t=0;ti||this.source.end.linen||this.source.end.line===i&&this.source.end.column{u();"use strict";ie.__esModule=!0;ie.UNIVERSAL=ie.TAG=ie.STRING=ie.SELECTOR=ie.ROOT=ie.PSEUDO=ie.NESTING=ie.ID=ie.COMMENT=ie.COMBINATOR=ie.CLASS=ie.ATTRIBUTE=void 0;var tk="tag";ie.TAG=tk;var rk="string";ie.STRING=rk;var ik="selector";ie.SELECTOR=ik;var nk="root";ie.ROOT=nk;var sk="pseudo";ie.PSEUDO=sk;var ak="nesting";ie.NESTING=ak;var ok="id";ie.ID=ok;var lk="comment";ie.COMMENT=lk;var uk="combinator";ie.COMBINATOR=uk;var fk="class";ie.CLASS=fk;var ck="attribute";ie.ATTRIBUTE=ck;var pk="universal";ie.UNIVERSAL=pk});var Ln=x((si,Fp)=>{u();"use strict";si.__esModule=!0;si.default=void 0;var dk=mk(dt()),ht=hk(Se());function Mp(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Mp=function(n){return n?t:e})(r)}function hk(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Mp(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function mk(r){return r&&r.__esModule?r:{default:r}}function gk(r,e){var t=typeof Symbol!="undefined"&&r[Symbol.iterator]||r["@@iterator"];if(t)return(t=t.call(r)).next.bind(t);if(Array.isArray(r)||(t=yk(r))||e&&r&&typeof r.length=="number"){t&&(r=t);var i=0;return function(){return i>=r.length?{done:!0}:{done:!1,value:r[i++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function yk(r,e){if(!!r){if(typeof r=="string")return Np(r,e);var t=Object.prototype.toString.call(r).slice(8,-1);if(t==="Object"&&r.constructor&&(t=r.constructor.name),t==="Map"||t==="Set")return Array.from(r);if(t==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t))return Np(r,e)}}function Np(r,e){(e==null||e>r.length)&&(e=r.length);for(var t=0,i=new Array(e);t=n&&(this.indexes[a]=s-1);return this},t.removeAll=function(){for(var n=gk(this.nodes),s;!(s=n()).done;){var a=s.value;a.parent=void 0}return this.nodes=[],this},t.empty=function(){return this.removeAll()},t.insertAfter=function(n,s){s.parent=this;var a=this.index(n);this.nodes.splice(a+1,0,s),s.parent=this;var o;for(var l in this.indexes)o=this.indexes[l],a<=o&&(this.indexes[l]=o+1);return this},t.insertBefore=function(n,s){s.parent=this;var a=this.index(n);this.nodes.splice(a,0,s),s.parent=this;var o;for(var l in this.indexes)o=this.indexes[l],o<=a&&(this.indexes[l]=o+1);return this},t._findChildAtPosition=function(n,s){var a=void 0;return this.each(function(o){if(o.atPosition){var l=o.atPosition(n,s);if(l)return a=l,!1}else if(o.isAtPosition(n,s))return a=o,!1}),a},t.atPosition=function(n,s){if(this.isAtPosition(n,s))return this._findChildAtPosition(n,s)||this},t._inferEndPosition=function(){this.last&&this.last.source&&this.last.source.end&&(this.source=this.source||{},this.source.end=this.source.end||{},Object.assign(this.source.end,this.last.source.end))},t.each=function(n){this.lastEach||(this.lastEach=0),this.indexes||(this.indexes={}),this.lastEach++;var s=this.lastEach;if(this.indexes[s]=0,!!this.length){for(var a,o;this.indexes[s]{u();"use strict";ai.__esModule=!0;ai.default=void 0;var xk=Sk(Ln()),kk=Se();function Sk(r){return r&&r.__esModule?r:{default:r}}function jp(r,e){for(var t=0;t{u();"use strict";oi.__esModule=!0;oi.default=void 0;var Ek=Tk(Ln()),Ok=Se();function Tk(r){return r&&r.__esModule?r:{default:r}}function Rk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,qa(r,e)}function qa(r,e){return qa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},qa(r,e)}var Pk=function(r){Rk(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Ok.SELECTOR,i}return e}(Ek.default);oi.default=Pk;Up.exports=oi.default});var Mn=x((_I,Vp)=>{u();"use strict";var Ik={},Dk=Ik.hasOwnProperty,qk=function(e,t){if(!e)return t;var i={};for(var n in t)i[n]=Dk.call(e,n)?e[n]:t[n];return i},$k=/[ -,\.\/:-@\[-\^`\{-~]/,Lk=/[ -,\.\/:-@\[\]\^`\{-~]/,Mk=/(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g,La=function r(e,t){t=qk(t,r.options),t.quotes!="single"&&t.quotes!="double"&&(t.quotes="single");for(var i=t.quotes=="double"?'"':"'",n=t.isIdentifier,s=e.charAt(0),a="",o=0,l=e.length;o126){if(f>=55296&&f<=56319&&o{u();"use strict";li.__esModule=!0;li.default=void 0;var Nk=Hp(Mn()),Bk=ii(),Fk=Hp(dt()),jk=Se();function Hp(r){return r&&r.__esModule?r:{default:r}}function Wp(r,e){for(var t=0;t{u();"use strict";ui.__esModule=!0;ui.default=void 0;var Hk=Gk(dt()),Wk=Se();function Gk(r){return r&&r.__esModule?r:{default:r}}function Qk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Ba(r,e)}function Ba(r,e){return Ba=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Ba(r,e)}var Yk=function(r){Qk(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Wk.COMMENT,i}return e}(Hk.default);ui.default=Yk;Qp.exports=ui.default});var za=x((fi,Yp)=>{u();"use strict";fi.__esModule=!0;fi.default=void 0;var Kk=Zk(dt()),Xk=Se();function Zk(r){return r&&r.__esModule?r:{default:r}}function Jk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ja(r,e)}function ja(r,e){return ja=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ja(r,e)}var eS=function(r){Jk(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=Xk.ID,n}var t=e.prototype;return t.valueToString=function(){return"#"+r.prototype.valueToString.call(this)},e}(Kk.default);fi.default=eS;Yp.exports=fi.default});var Nn=x((ci,Zp)=>{u();"use strict";ci.__esModule=!0;ci.default=void 0;var tS=Kp(Mn()),rS=ii(),iS=Kp(dt());function Kp(r){return r&&r.__esModule?r:{default:r}}function Xp(r,e){for(var t=0;t{u();"use strict";pi.__esModule=!0;pi.default=void 0;var oS=uS(Nn()),lS=Se();function uS(r){return r&&r.__esModule?r:{default:r}}function fS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Va(r,e)}function Va(r,e){return Va=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Va(r,e)}var cS=function(r){fS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=lS.TAG,i}return e}(oS.default);pi.default=cS;Jp.exports=pi.default});var Ga=x((di,ed)=>{u();"use strict";di.__esModule=!0;di.default=void 0;var pS=hS(dt()),dS=Se();function hS(r){return r&&r.__esModule?r:{default:r}}function mS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Wa(r,e)}function Wa(r,e){return Wa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Wa(r,e)}var gS=function(r){mS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=dS.STRING,i}return e}(pS.default);di.default=gS;ed.exports=di.default});var Ya=x((hi,td)=>{u();"use strict";hi.__esModule=!0;hi.default=void 0;var yS=wS(Ln()),bS=Se();function wS(r){return r&&r.__esModule?r:{default:r}}function vS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Qa(r,e)}function Qa(r,e){return Qa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Qa(r,e)}var xS=function(r){vS(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=bS.PSEUDO,n}var t=e.prototype;return t.toString=function(){var n=this.length?"("+this.map(String).join(",")+")":"";return[this.rawSpaceBefore,this.stringifyProperty("value"),n,this.rawSpaceAfter].join("")},e}(yS.default);hi.default=xS;td.exports=hi.default});var Bn={};Ge(Bn,{deprecate:()=>kS});function kS(r){return r}var Fn=P(()=>{u()});var id=x((EI,rd)=>{u();rd.exports=(Fn(),Bn).deprecate});var to=x(yi=>{u();"use strict";yi.__esModule=!0;yi.default=void 0;yi.unescapeValue=Ja;var mi=Xa(Mn()),SS=Xa(Pn()),AS=Xa(Nn()),CS=Se(),Ka;function Xa(r){return r&&r.__esModule?r:{default:r}}function nd(r,e){for(var t=0;t0&&!n.quoted&&o.before.length===0&&!(n.spaces.value&&n.spaces.value.after)&&(o.before=" "),sd(a,o)}))),s.push("]"),s.push(this.rawSpaceAfter),s.join("")},_S(e,[{key:"quoted",get:function(){var n=this.quoteMark;return n==="'"||n==='"'},set:function(n){RS()}},{key:"quoteMark",get:function(){return this._quoteMark},set:function(n){if(!this._constructed){this._quoteMark=n;return}this._quoteMark!==n&&(this._quoteMark=n,this._syncRawValue())}},{key:"qualifiedAttribute",get:function(){return this.qualifiedName(this.raws.attribute||this.attribute)}},{key:"insensitiveFlag",get:function(){return this.insensitive?"i":""}},{key:"value",get:function(){return this._value},set:function(n){if(this._constructed){var s=Ja(n),a=s.deprecatedUsage,o=s.unescaped,l=s.quoteMark;if(a&&TS(),o===this._value&&l===this._quoteMark)return;this._value=o,this._quoteMark=l,this._syncRawValue()}else this._value=n}},{key:"insensitive",get:function(){return this._insensitive},set:function(n){n||(this._insensitive=!1,this.raws&&(this.raws.insensitiveFlag==="I"||this.raws.insensitiveFlag==="i")&&(this.raws.insensitiveFlag=void 0)),this._insensitive=n}},{key:"attribute",get:function(){return this._attribute},set:function(n){this._handleEscapes("attribute",n),this._attribute=n}}]),e}(AS.default);yi.default=jn;jn.NO_QUOTE=null;jn.SINGLE_QUOTE="'";jn.DOUBLE_QUOTE='"';var eo=(Ka={"'":{quotes:"single",wrap:!0},'"':{quotes:"double",wrap:!0}},Ka[null]={isIdentifier:!0},Ka);function sd(r,e){return""+e.before+r+e.after}});var io=x((bi,ad)=>{u();"use strict";bi.__esModule=!0;bi.default=void 0;var DS=$S(Nn()),qS=Se();function $S(r){return r&&r.__esModule?r:{default:r}}function LS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ro(r,e)}function ro(r,e){return ro=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ro(r,e)}var MS=function(r){LS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=qS.UNIVERSAL,i.value="*",i}return e}(DS.default);bi.default=MS;ad.exports=bi.default});var so=x((wi,od)=>{u();"use strict";wi.__esModule=!0;wi.default=void 0;var NS=FS(dt()),BS=Se();function FS(r){return r&&r.__esModule?r:{default:r}}function jS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,no(r,e)}function no(r,e){return no=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},no(r,e)}var zS=function(r){jS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=BS.COMBINATOR,i}return e}(NS.default);wi.default=zS;od.exports=wi.default});var oo=x((vi,ld)=>{u();"use strict";vi.__esModule=!0;vi.default=void 0;var US=HS(dt()),VS=Se();function HS(r){return r&&r.__esModule?r:{default:r}}function WS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ao(r,e)}function ao(r,e){return ao=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ao(r,e)}var GS=function(r){WS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=VS.NESTING,i.value="&",i}return e}(US.default);vi.default=GS;ld.exports=vi.default});var fd=x((zn,ud)=>{u();"use strict";zn.__esModule=!0;zn.default=QS;function QS(r){return r.sort(function(e,t){return e-t})}ud.exports=zn.default});var lo=x(M=>{u();"use strict";M.__esModule=!0;M.word=M.tilde=M.tab=M.str=M.space=M.slash=M.singleQuote=M.semicolon=M.plus=M.pipe=M.openSquare=M.openParenthesis=M.newline=M.greaterThan=M.feed=M.equals=M.doubleQuote=M.dollar=M.cr=M.comment=M.comma=M.combinator=M.colon=M.closeSquare=M.closeParenthesis=M.caret=M.bang=M.backslash=M.at=M.asterisk=M.ampersand=void 0;var YS=38;M.ampersand=YS;var KS=42;M.asterisk=KS;var XS=64;M.at=XS;var ZS=44;M.comma=ZS;var JS=58;M.colon=JS;var eA=59;M.semicolon=eA;var tA=40;M.openParenthesis=tA;var rA=41;M.closeParenthesis=rA;var iA=91;M.openSquare=iA;var nA=93;M.closeSquare=nA;var sA=36;M.dollar=sA;var aA=126;M.tilde=aA;var oA=94;M.caret=oA;var lA=43;M.plus=lA;var uA=61;M.equals=uA;var fA=124;M.pipe=fA;var cA=62;M.greaterThan=cA;var pA=32;M.space=pA;var cd=39;M.singleQuote=cd;var dA=34;M.doubleQuote=dA;var hA=47;M.slash=hA;var mA=33;M.bang=mA;var gA=92;M.backslash=gA;var yA=13;M.cr=yA;var bA=12;M.feed=bA;var wA=10;M.newline=wA;var vA=9;M.tab=vA;var xA=cd;M.str=xA;var kA=-1;M.comment=kA;var SA=-2;M.word=SA;var AA=-3;M.combinator=AA});var hd=x(xi=>{u();"use strict";xi.__esModule=!0;xi.FIELDS=void 0;xi.default=PA;var D=CA(lo()),nr,te;function pd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(pd=function(n){return n?t:e})(r)}function CA(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=pd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}var _A=(nr={},nr[D.tab]=!0,nr[D.newline]=!0,nr[D.cr]=!0,nr[D.feed]=!0,nr),EA=(te={},te[D.space]=!0,te[D.tab]=!0,te[D.newline]=!0,te[D.cr]=!0,te[D.feed]=!0,te[D.ampersand]=!0,te[D.asterisk]=!0,te[D.bang]=!0,te[D.comma]=!0,te[D.colon]=!0,te[D.semicolon]=!0,te[D.openParenthesis]=!0,te[D.closeParenthesis]=!0,te[D.openSquare]=!0,te[D.closeSquare]=!0,te[D.singleQuote]=!0,te[D.doubleQuote]=!0,te[D.plus]=!0,te[D.pipe]=!0,te[D.tilde]=!0,te[D.greaterThan]=!0,te[D.equals]=!0,te[D.dollar]=!0,te[D.caret]=!0,te[D.slash]=!0,te),uo={},dd="0123456789abcdefABCDEF";for(Un=0;Un0?(k=a+v,S=w-y[v].length):(k=a,S=s),T=D.comment,a=k,p=k,d=w-S):c===D.slash?(w=o,T=c,p=a,d=o-s,l=w+1):(w=OA(t,o),T=D.word,p=a,d=w-s),l=w+1;break}e.push([T,a,o-s,p,d,o,l]),S&&(s=S,S=null),o=l}return e}});var kd=x((ki,xd)=>{u();"use strict";ki.__esModule=!0;ki.default=void 0;var IA=je(Da()),fo=je($a()),DA=je(Na()),md=je(Fa()),qA=je(za()),$A=je(Ha()),co=je(Ga()),LA=je(Ya()),gd=Vn(to()),MA=je(io()),po=je(so()),NA=je(oo()),BA=je(fd()),O=Vn(hd()),q=Vn(lo()),FA=Vn(Se()),ue=ii(),Vt,ho;function yd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(yd=function(n){return n?t:e})(r)}function Vn(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=yd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function je(r){return r&&r.__esModule?r:{default:r}}function bd(r,e){for(var t=0;t0){var a=this.current.last;if(a){var o=this.convertWhitespaceNodesToSpace(s),l=o.space,c=o.rawSpace;c!==void 0&&(a.rawSpaceAfter+=c),a.spaces.after+=l}else s.forEach(function(T){return i.newNode(T)})}return}var f=this.currToken,d=void 0;n>this.position&&(d=this.parseWhitespaceEquivalentTokens(n));var p;if(this.isNamedCombinator()?p=this.namedCombinator():this.currToken[O.FIELDS.TYPE]===q.combinator?(p=new po.default({value:this.content(),source:sr(this.currToken),sourceIndex:this.currToken[O.FIELDS.START_POS]}),this.position++):mo[this.currToken[O.FIELDS.TYPE]]||d||this.unexpected(),p){if(d){var h=this.convertWhitespaceNodesToSpace(d),b=h.space,v=h.rawSpace;p.spaces.before=b,p.rawSpaceBefore=v}}else{var y=this.convertWhitespaceNodesToSpace(d,!0),w=y.space,k=y.rawSpace;k||(k=w);var S={},E={spaces:{}};w.endsWith(" ")&&k.endsWith(" ")?(S.before=w.slice(0,w.length-1),E.spaces.before=k.slice(0,k.length-1)):w.startsWith(" ")&&k.startsWith(" ")?(S.after=w.slice(1),E.spaces.after=k.slice(1)):E.value=k,p=new po.default({value:" ",source:go(f,this.tokens[this.position-1]),sourceIndex:f[O.FIELDS.START_POS],spaces:S,raws:E})}return this.currToken&&this.currToken[O.FIELDS.TYPE]===q.space&&(p.spaces.after=this.optionalSpace(this.content()),this.position++),this.newNode(p)},e.comma=function(){if(this.position===this.tokens.length-1){this.root.trailingComma=!0,this.position++;return}this.current._inferEndPosition();var i=new fo.default({source:{start:wd(this.tokens[this.position+1])},sourceIndex:this.tokens[this.position+1][O.FIELDS.START_POS]});this.current.parent.append(i),this.current=i,this.position++},e.comment=function(){var i=this.currToken;this.newNode(new md.default({value:this.content(),source:sr(i),sourceIndex:i[O.FIELDS.START_POS]})),this.position++},e.error=function(i,n){throw this.root.error(i,n)},e.missingBackslash=function(){return this.error("Expected a backslash preceding the semicolon.",{index:this.currToken[O.FIELDS.START_POS]})},e.missingParenthesis=function(){return this.expected("opening parenthesis",this.currToken[O.FIELDS.START_POS])},e.missingSquareBracket=function(){return this.expected("opening square bracket",this.currToken[O.FIELDS.START_POS])},e.unexpected=function(){return this.error("Unexpected '"+this.content()+"'. Escaping special characters with \\ may help.",this.currToken[O.FIELDS.START_POS])},e.unexpectedPipe=function(){return this.error("Unexpected '|'.",this.currToken[O.FIELDS.START_POS])},e.namespace=function(){var i=this.prevToken&&this.content(this.prevToken)||!0;if(this.nextToken[O.FIELDS.TYPE]===q.word)return this.position++,this.word(i);if(this.nextToken[O.FIELDS.TYPE]===q.asterisk)return this.position++,this.universal(i);this.unexpectedPipe()},e.nesting=function(){if(this.nextToken){var i=this.content(this.nextToken);if(i==="|"){this.position++;return}}var n=this.currToken;this.newNode(new NA.default({value:this.content(),source:sr(n),sourceIndex:n[O.FIELDS.START_POS]})),this.position++},e.parentheses=function(){var i=this.current.last,n=1;if(this.position++,i&&i.type===FA.PSEUDO){var s=new fo.default({source:{start:wd(this.tokens[this.position])},sourceIndex:this.tokens[this.position][O.FIELDS.START_POS]}),a=this.current;for(i.append(s),this.current=s;this.position1&&i.nextToken&&i.nextToken[O.FIELDS.TYPE]===q.openParenthesis&&i.error("Misplaced parenthesis.",{index:i.nextToken[O.FIELDS.START_POS]})});else return this.expected(["pseudo-class","pseudo-element"],this.currToken[O.FIELDS.START_POS])},e.space=function(){var i=this.content();this.position===0||this.prevToken[O.FIELDS.TYPE]===q.comma||this.prevToken[O.FIELDS.TYPE]===q.openParenthesis||this.current.nodes.every(function(n){return n.type==="comment"})?(this.spaces=this.optionalSpace(i),this.position++):this.position===this.tokens.length-1||this.nextToken[O.FIELDS.TYPE]===q.comma||this.nextToken[O.FIELDS.TYPE]===q.closeParenthesis?(this.current.last.spaces.after=this.optionalSpace(i),this.position++):this.combinator()},e.string=function(){var i=this.currToken;this.newNode(new co.default({value:this.content(),source:sr(i),sourceIndex:i[O.FIELDS.START_POS]})),this.position++},e.universal=function(i){var n=this.nextToken;if(n&&this.content(n)==="|")return this.position++,this.namespace();var s=this.currToken;this.newNode(new MA.default({value:this.content(),source:sr(s),sourceIndex:s[O.FIELDS.START_POS]}),i),this.position++},e.splitWord=function(i,n){for(var s=this,a=this.nextToken,o=this.content();a&&~[q.dollar,q.caret,q.equals,q.word].indexOf(a[O.FIELDS.TYPE]);){this.position++;var l=this.content();if(o+=l,l.lastIndexOf("\\")===l.length-1){var c=this.nextToken;c&&c[O.FIELDS.TYPE]===q.space&&(o+=this.requiredSpace(this.content(c)),this.position++)}a=this.nextToken}var f=yo(o,".").filter(function(b){var v=o[b-1]==="\\",y=/^\d+\.\d+%$/.test(o);return!v&&!y}),d=yo(o,"#").filter(function(b){return o[b-1]!=="\\"}),p=yo(o,"#{");p.length&&(d=d.filter(function(b){return!~p.indexOf(b)}));var h=(0,BA.default)(UA([0].concat(f,d)));h.forEach(function(b,v){var y=h[v+1]||o.length,w=o.slice(b,y);if(v===0&&n)return n.call(s,w,h.length);var k,S=s.currToken,E=S[O.FIELDS.START_POS]+h[v],T=Ht(S[1],S[2]+b,S[3],S[2]+(y-1));if(~f.indexOf(b)){var B={value:w.slice(1),source:T,sourceIndex:E};k=new DA.default(ar(B,"value"))}else if(~d.indexOf(b)){var N={value:w.slice(1),source:T,sourceIndex:E};k=new qA.default(ar(N,"value"))}else{var R={value:w,source:T,sourceIndex:E};ar(R,"value"),k=new $A.default(R)}s.newNode(k,i),i=null}),this.position++},e.word=function(i){var n=this.nextToken;return n&&this.content(n)==="|"?(this.position++,this.namespace()):this.splitWord(i)},e.loop=function(){for(;this.position{u();"use strict";Si.__esModule=!0;Si.default=void 0;var HA=WA(kd());function WA(r){return r&&r.__esModule?r:{default:r}}var GA=function(){function r(t,i){this.func=t||function(){},this.funcRes=null,this.options=i}var e=r.prototype;return e._shouldUpdateSelector=function(i,n){n===void 0&&(n={});var s=Object.assign({},this.options,n);return s.updateSelector===!1?!1:typeof i!="string"},e._isLossy=function(i){i===void 0&&(i={});var n=Object.assign({},this.options,i);return n.lossless===!1},e._root=function(i,n){n===void 0&&(n={});var s=new HA.default(i,this._parseOptions(n));return s.root},e._parseOptions=function(i){return{lossy:this._isLossy(i)}},e._run=function(i,n){var s=this;return n===void 0&&(n={}),new Promise(function(a,o){try{var l=s._root(i,n);Promise.resolve(s.func(l)).then(function(c){var f=void 0;return s._shouldUpdateSelector(i,n)&&(f=l.toString(),i.selector=f),{transform:c,root:l,string:f}}).then(a,o)}catch(c){o(c);return}})},e._runSync=function(i,n){n===void 0&&(n={});var s=this._root(i,n),a=this.func(s);if(a&&typeof a.then=="function")throw new Error("Selector processor returned a promise to a synchronous call.");var o=void 0;return n.updateSelector&&typeof i!="string"&&(o=s.toString(),i.selector=o),{transform:a,root:s,string:o}},e.ast=function(i,n){return this._run(i,n).then(function(s){return s.root})},e.astSync=function(i,n){return this._runSync(i,n).root},e.transform=function(i,n){return this._run(i,n).then(function(s){return s.transform})},e.transformSync=function(i,n){return this._runSync(i,n).transform},e.process=function(i,n){return this._run(i,n).then(function(s){return s.string||s.root.toString()})},e.processSync=function(i,n){var s=this._runSync(i,n);return s.string||s.root.toString()},r}();Si.default=GA;Sd.exports=Si.default});var Cd=x(ne=>{u();"use strict";ne.__esModule=!0;ne.universal=ne.tag=ne.string=ne.selector=ne.root=ne.pseudo=ne.nesting=ne.id=ne.comment=ne.combinator=ne.className=ne.attribute=void 0;var QA=ze(to()),YA=ze(Na()),KA=ze(so()),XA=ze(Fa()),ZA=ze(za()),JA=ze(oo()),eC=ze(Ya()),tC=ze(Da()),rC=ze($a()),iC=ze(Ga()),nC=ze(Ha()),sC=ze(io());function ze(r){return r&&r.__esModule?r:{default:r}}var aC=function(e){return new QA.default(e)};ne.attribute=aC;var oC=function(e){return new YA.default(e)};ne.className=oC;var lC=function(e){return new KA.default(e)};ne.combinator=lC;var uC=function(e){return new XA.default(e)};ne.comment=uC;var fC=function(e){return new ZA.default(e)};ne.id=fC;var cC=function(e){return new JA.default(e)};ne.nesting=cC;var pC=function(e){return new eC.default(e)};ne.pseudo=pC;var dC=function(e){return new tC.default(e)};ne.root=dC;var hC=function(e){return new rC.default(e)};ne.selector=hC;var mC=function(e){return new iC.default(e)};ne.string=mC;var gC=function(e){return new nC.default(e)};ne.tag=gC;var yC=function(e){return new sC.default(e)};ne.universal=yC});var Td=x(Z=>{u();"use strict";Z.__esModule=!0;Z.isComment=Z.isCombinator=Z.isClassName=Z.isAttribute=void 0;Z.isContainer=TC;Z.isIdentifier=void 0;Z.isNamespace=RC;Z.isNesting=void 0;Z.isNode=bo;Z.isPseudo=void 0;Z.isPseudoClass=OC;Z.isPseudoElement=Od;Z.isUniversal=Z.isTag=Z.isString=Z.isSelector=Z.isRoot=void 0;var fe=Se(),Oe,bC=(Oe={},Oe[fe.ATTRIBUTE]=!0,Oe[fe.CLASS]=!0,Oe[fe.COMBINATOR]=!0,Oe[fe.COMMENT]=!0,Oe[fe.ID]=!0,Oe[fe.NESTING]=!0,Oe[fe.PSEUDO]=!0,Oe[fe.ROOT]=!0,Oe[fe.SELECTOR]=!0,Oe[fe.STRING]=!0,Oe[fe.TAG]=!0,Oe[fe.UNIVERSAL]=!0,Oe);function bo(r){return typeof r=="object"&&bC[r.type]}function Ue(r,e){return bo(e)&&e.type===r}var _d=Ue.bind(null,fe.ATTRIBUTE);Z.isAttribute=_d;var wC=Ue.bind(null,fe.CLASS);Z.isClassName=wC;var vC=Ue.bind(null,fe.COMBINATOR);Z.isCombinator=vC;var xC=Ue.bind(null,fe.COMMENT);Z.isComment=xC;var kC=Ue.bind(null,fe.ID);Z.isIdentifier=kC;var SC=Ue.bind(null,fe.NESTING);Z.isNesting=SC;var wo=Ue.bind(null,fe.PSEUDO);Z.isPseudo=wo;var AC=Ue.bind(null,fe.ROOT);Z.isRoot=AC;var CC=Ue.bind(null,fe.SELECTOR);Z.isSelector=CC;var _C=Ue.bind(null,fe.STRING);Z.isString=_C;var Ed=Ue.bind(null,fe.TAG);Z.isTag=Ed;var EC=Ue.bind(null,fe.UNIVERSAL);Z.isUniversal=EC;function Od(r){return wo(r)&&r.value&&(r.value.startsWith("::")||r.value.toLowerCase()===":before"||r.value.toLowerCase()===":after"||r.value.toLowerCase()===":first-letter"||r.value.toLowerCase()===":first-line")}function OC(r){return wo(r)&&!Od(r)}function TC(r){return!!(bo(r)&&r.walk)}function RC(r){return _d(r)||Ed(r)}});var Rd=x(Ke=>{u();"use strict";Ke.__esModule=!0;var vo=Se();Object.keys(vo).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===vo[r]||(Ke[r]=vo[r])});var xo=Cd();Object.keys(xo).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===xo[r]||(Ke[r]=xo[r])});var ko=Td();Object.keys(ko).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===ko[r]||(Ke[r]=ko[r])})});var it=x((Ai,Id)=>{u();"use strict";Ai.__esModule=!0;Ai.default=void 0;var PC=qC(Ad()),IC=DC(Rd());function Pd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Pd=function(n){return n?t:e})(r)}function DC(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Pd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function qC(r){return r&&r.__esModule?r:{default:r}}var So=function(e){return new PC.default(e)};Object.assign(So,IC);delete So.__esModule;var $C=So;Ai.default=$C;Id.exports=Ai.default});function mt(r){return["fontSize","outline"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e[0]),e):r==="fontFamily"?e=>{typeof e=="function"&&(e=e({}));let t=Array.isArray(e)&&ke(e[1])?e[0]:e;return Array.isArray(t)?t.join(", "):t}:["boxShadow","transitionProperty","transitionDuration","transitionDelay","transitionTimingFunction","backgroundImage","backgroundSize","backgroundColor","cursor","animation"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e.join(", ")),e):["gridTemplateColumns","gridTemplateRows","objectPosition"].includes(r)?e=>(typeof e=="function"&&(e=e({})),typeof e=="string"&&(e=ee.list.comma(e).join(" ")),e):(e,t={})=>(typeof e=="function"&&(e=e(t)),e)}var Ci=P(()=>{u();Ot();Kt()});var Bd=x((MI,Oo)=>{u();var{AtRule:LC,Rule:Dd}=$e(),qd=it();function Ao(r,e){let t;try{qd(i=>{t=i}).processSync(r)}catch(i){throw r.includes(":")?e?e.error("Missed semicolon"):i:e?e.error(i.message):i}return t.at(0)}function $d(r,e){let t=!1;return r.each(i=>{if(i.type==="nesting"){let n=e.clone({});i.value!=="&"?i.replaceWith(Ao(i.value.replace("&",n.toString()))):i.replaceWith(n),t=!0}else"nodes"in i&&i.nodes&&$d(i,e)&&(t=!0)}),t}function Ld(r,e){let t=[];return r.selectors.forEach(i=>{let n=Ao(i,r);e.selectors.forEach(s=>{if(!s)return;let a=Ao(s,e);$d(a,n)||(a.prepend(qd.combinator({value:" "})),a.prepend(n.clone({}))),t.push(a.toString())})}),t}function Hn(r,e){let t=r.prev();for(e.after(r);t&&t.type==="comment";){let i=t.prev();e.after(t),t=i}return r}function MC(r){return function e(t,i,n,s=n){let a=[];if(i.each(o=>{o.type==="rule"&&n?s&&(o.selectors=Ld(t,o)):o.type==="atrule"&&o.nodes?r[o.name]?e(t,o,s):i[_o]!==!1&&a.push(o):a.push(o)}),n&&a.length){let o=t.clone({nodes:[]});for(let l of a)o.append(l);i.prepend(o)}}}function Co(r,e,t){let i=new Dd({nodes:[],selector:r});return i.append(e),t.after(i),i}function Md(r,e){let t={};for(let i of r)t[i]=!0;if(e)for(let i of e)t[i.replace(/^@/,"")]=!0;return t}function NC(r){r=r.trim();let e=r.match(/^\((.*)\)$/);if(!e)return{selector:r,type:"basic"};let t=e[1].match(/^(with(?:out)?):(.+)$/);if(t){let i=t[1]==="with",n=Object.fromEntries(t[2].trim().split(/\s+/).map(a=>[a,!0]));if(i&&n.all)return{type:"noop"};let s=a=>!!n[a];return n.all?s=()=>!0:i&&(s=a=>a==="all"?!1:!n[a]),{escapes:s,type:"withrules"}}return{type:"unknown"}}function BC(r){let e=[],t=r.parent;for(;t&&t instanceof LC;)e.push(t),t=t.parent;return e}function FC(r){let e=r[Nd];if(!e)r.after(r.nodes);else{let t=r.nodes,i,n=-1,s,a,o,l=BC(r);if(l.forEach((c,f)=>{if(e(c.name))i=c,n=f,a=o;else{let d=o;o=c.clone({nodes:[]}),d&&o.append(d),s=s||o}}),i?a?(s.append(t),i.after(a)):i.after(t):r.after(t),r.next()&&i){let c;l.slice(0,n+1).forEach((f,d,p)=>{let h=c;c=f.clone({nodes:[]}),h&&c.append(h);let b=[],y=(p[d-1]||r).next();for(;y;)b.push(y),y=y.next();c.append(b)}),c&&(a||t[t.length-1]).after(c)}}r.remove()}var _o=Symbol("rootRuleMergeSel"),Nd=Symbol("rootRuleEscapes");function jC(r){let{params:e}=r,{escapes:t,selector:i,type:n}=NC(e);if(n==="unknown")throw r.error(`Unknown @${r.name} parameter ${JSON.stringify(e)}`);if(n==="basic"&&i){let s=new Dd({nodes:r.nodes,selector:i});r.removeAll(),r.append(s)}r[Nd]=t,r[_o]=t?!t("all"):n==="noop"}var Eo=Symbol("hasRootRule");Oo.exports=(r={})=>{let e=Md(["media","supports","layer","container","starting-style"],r.bubble),t=MC(e),i=Md(["document","font-face","keyframes","-webkit-keyframes","-moz-keyframes"],r.unwrap),n=(r.rootRuleName||"at-root").replace(/^@/,""),s=r.preserveEmpty;return{Once(a){a.walkAtRules(n,o=>{jC(o),a[Eo]=!0})},postcssPlugin:"postcss-nested",RootExit(a){a[Eo]&&(a.walkAtRules(n,FC),a[Eo]=!1)},Rule(a){let o=!1,l=a,c=!1,f=[];a.each(d=>{d.type==="rule"?(f.length&&(l=Co(a.selector,f,l),f=[]),c=!0,o=!0,d.selectors=Ld(a,d),l=Hn(d,l)):d.type==="atrule"?(f.length&&(l=Co(a.selector,f,l),f=[]),d.name===n?(o=!0,t(a,d,!0,d[_o]),l=Hn(d,l)):e[d.name]?(c=!0,o=!0,t(a,d,!0),l=Hn(d,l)):i[d.name]?(c=!0,o=!0,t(a,d,!1),l=Hn(d,l)):c&&f.push(d)):d.type==="decl"&&c&&f.push(d)}),f.length&&(l=Co(a.selector,f,l)),o&&s!==!0&&(a.raws.semicolon=!0,a.nodes.length===0&&a.remove())}}};Oo.exports.postcss=!0});var Ud=x((NI,zd)=>{u();"use strict";var Fd=/-(\w|$)/g,jd=(r,e)=>e.toUpperCase(),zC=r=>(r=r.toLowerCase(),r==="float"?"cssFloat":r.startsWith("-ms-")?r.substr(1).replace(Fd,jd):r.replace(Fd,jd));zd.exports=zC});var Po=x((BI,Vd)=>{u();var UC=Ud(),VC={boxFlex:!0,boxFlexGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,strokeDashoffset:!0,strokeOpacity:!0,strokeWidth:!0};function To(r){return typeof r.nodes=="undefined"?!0:Ro(r)}function Ro(r){let e,t={};return r.each(i=>{if(i.type==="atrule")e="@"+i.name,i.params&&(e+=" "+i.params),typeof t[e]=="undefined"?t[e]=To(i):Array.isArray(t[e])?t[e].push(To(i)):t[e]=[t[e],To(i)];else if(i.type==="rule"){let n=Ro(i);if(t[i.selector])for(let s in n)t[i.selector][s]=n[s];else t[i.selector]=n}else if(i.type==="decl"){i.prop[0]==="-"&&i.prop[1]==="-"||i.parent&&i.parent.selector===":export"?e=i.prop:e=UC(i.prop);let n=i.value;!isNaN(i.value)&&VC[e]&&(n=parseFloat(i.value)),i.important&&(n+=" !important"),typeof t[e]=="undefined"?t[e]=n:Array.isArray(t[e])?t[e].push(n):t[e]=[t[e],n]}}),t}Vd.exports=Ro});var Wn=x((FI,Qd)=>{u();var _i=$e(),Hd=/\s*!important\s*$/i,HC={"box-flex":!0,"box-flex-group":!0,"column-count":!0,flex:!0,"flex-grow":!0,"flex-positive":!0,"flex-shrink":!0,"flex-negative":!0,"font-weight":!0,"line-clamp":!0,"line-height":!0,opacity:!0,order:!0,orphans:!0,"tab-size":!0,widows:!0,"z-index":!0,zoom:!0,"fill-opacity":!0,"stroke-dashoffset":!0,"stroke-opacity":!0,"stroke-width":!0};function WC(r){return r.replace(/([A-Z])/g,"-$1").replace(/^ms-/,"-ms-").toLowerCase()}function Wd(r,e,t){t===!1||t===null||(e.startsWith("--")||(e=WC(e)),typeof t=="number"&&(t===0||HC[e]?t=t.toString():t+="px"),e==="css-float"&&(e="float"),Hd.test(t)?(t=t.replace(Hd,""),r.push(_i.decl({prop:e,value:t,important:!0}))):r.push(_i.decl({prop:e,value:t})))}function Gd(r,e,t){let i=_i.atRule({name:e[1],params:e[3]||""});typeof t=="object"&&(i.nodes=[],Io(t,i)),r.push(i)}function Io(r,e){let t,i,n;for(t in r)if(i=r[t],!(i===null||typeof i=="undefined"))if(t[0]==="@"){let s=t.match(/@(\S+)(\s+([\W\w]*)\s*)?/);if(Array.isArray(i))for(let a of i)Gd(e,s,a);else Gd(e,s,i)}else if(Array.isArray(i))for(let s of i)Wd(e,t,s);else typeof i=="object"?(n=_i.rule({selector:t}),Io(i,n),e.push(n)):Wd(e,t,i)}Qd.exports=function(r){let e=_i.root();return Io(r,e),e}});var Do=x((jI,Yd)=>{u();var GC=Po();Yd.exports=function(e){return console&&console.warn&&e.warnings().forEach(t=>{let i=t.plugin||"PostCSS";console.warn(i+": "+t.text)}),GC(e.root)}});var Xd=x((zI,Kd)=>{u();var QC=$e(),YC=Do(),KC=Wn();Kd.exports=function(e){let t=QC(e);return async i=>{let n=await t.process(i,{parser:KC,from:void 0});return YC(n)}}});var Jd=x((UI,Zd)=>{u();var XC=$e(),ZC=Do(),JC=Wn();Zd.exports=function(r){let e=XC(r);return t=>{let i=e.process(t,{parser:JC,from:void 0});return ZC(i)}}});var th=x((VI,eh)=>{u();var e_=Po(),t_=Wn(),r_=Xd(),i_=Jd();eh.exports={objectify:e_,parse:t_,async:r_,sync:i_}});var or,rh,HI,WI,GI,QI,ih=P(()=>{u();or=pe(th()),rh=or.default,HI=or.default.objectify,WI=or.default.parse,GI=or.default.async,QI=or.default.sync});function lr(r){return Array.isArray(r)?r.flatMap(e=>ee([(0,nh.default)({bubble:["screen"]})]).process(e,{parser:rh}).root.nodes):lr([r])}var nh,qo=P(()=>{u();Ot();nh=pe(Bd());ih()});function ur(r,e,t=!1){if(r==="")return e;let i=typeof e=="string"?(0,sh.default)().astSync(e):e;return i.walkClasses(n=>{let s=n.value,a=t&&s.startsWith("-");n.value=a?`-${r}${s.slice(1)}`:`${r}${s}`}),typeof e=="string"?i.toString():i}var sh,Gn=P(()=>{u();sh=pe(it())});function Te(r){let e=ah.default.className();return e.value=r,jt(e?.raws?.value??e.value)}var ah,fr=P(()=>{u();ah=pe(it());Zi()});function $o(r){return jt(`.${Te(r)}`)}function Qn(r,e){return $o(Ei(r,e))}function Ei(r,e){return e==="DEFAULT"?r:e==="-"||e==="-DEFAULT"?`-${r}`:e.startsWith("-")?`-${r}${e}`:e.startsWith("/")?`${r}${e}`:`${r}-${e}`}var Lo=P(()=>{u();fr();Zi()});function L(r,e=[[r,[r]]],{filterDefault:t=!1,...i}={}){let n=mt(r);return function({matchUtilities:s,theme:a}){for(let o of e){let l=Array.isArray(o[0])?o:[o];s(l.reduce((c,[f,d])=>Object.assign(c,{[f]:p=>d.reduce((h,b)=>Array.isArray(b)?Object.assign(h,{[b[0]]:b[1]}):Object.assign(h,{[b]:n(p)}),{})}),{}),{...i,values:t?Object.fromEntries(Object.entries(a(r)??{}).filter(([c])=>c!=="DEFAULT")):a(r)})}}}var oh=P(()=>{u();Ci()});function Tt(r){return r=Array.isArray(r)?r:[r],r.map(e=>{let t=e.values.map(i=>i.raw!==void 0?i.raw:[i.min&&`(min-width: ${i.min})`,i.max&&`(max-width: ${i.max})`].filter(Boolean).join(" and "));return e.not?`not all and ${t}`:t}).join(", ")}var Yn=P(()=>{u()});function Mo(r){return r.split(f_).map(t=>{let i=t.trim(),n={value:i},s=i.split(c_),a=new Set;for(let o of s)!a.has("DIRECTIONS")&&n_.has(o)?(n.direction=o,a.add("DIRECTIONS")):!a.has("PLAY_STATES")&&s_.has(o)?(n.playState=o,a.add("PLAY_STATES")):!a.has("FILL_MODES")&&a_.has(o)?(n.fillMode=o,a.add("FILL_MODES")):!a.has("ITERATION_COUNTS")&&(o_.has(o)||p_.test(o))?(n.iterationCount=o,a.add("ITERATION_COUNTS")):!a.has("TIMING_FUNCTION")&&l_.has(o)||!a.has("TIMING_FUNCTION")&&u_.some(l=>o.startsWith(`${l}(`))?(n.timingFunction=o,a.add("TIMING_FUNCTION")):!a.has("DURATION")&&lh.test(o)?(n.duration=o,a.add("DURATION")):!a.has("DELAY")&&lh.test(o)?(n.delay=o,a.add("DELAY")):a.has("NAME")?(n.unknown||(n.unknown=[]),n.unknown.push(o)):(n.name=o,a.add("NAME"));return n})}var n_,s_,a_,o_,l_,u_,f_,c_,lh,p_,uh=P(()=>{u();n_=new Set(["normal","reverse","alternate","alternate-reverse"]),s_=new Set(["running","paused"]),a_=new Set(["none","forwards","backwards","both"]),o_=new Set(["infinite"]),l_=new Set(["linear","ease","ease-in","ease-out","ease-in-out","step-start","step-end"]),u_=["cubic-bezier","steps"],f_=/\,(?![^(]*\))/g,c_=/\ +(?![^(]*\))/g,lh=/^(-?[\d.]+m?s)$/,p_=/^(\d+)$/});var fh,xe,ch=P(()=>{u();fh=r=>Object.assign({},...Object.entries(r??{}).flatMap(([e,t])=>typeof t=="object"?Object.entries(fh(t)).map(([i,n])=>({[e+(i==="DEFAULT"?"":`-${i}`)]:n})):[{[`${e}`]:t}])),xe=fh});var dh,ph=P(()=>{dh="3.4.17"});function Rt(r,e=!0){return Array.isArray(r)?r.map(t=>{if(e&&Array.isArray(t))throw new Error("The tuple syntax is not supported for `screens`.");if(typeof t=="string")return{name:t.toString(),not:!1,values:[{min:t,max:void 0}]};let[i,n]=t;return i=i.toString(),typeof n=="string"?{name:i,not:!1,values:[{min:n,max:void 0}]}:Array.isArray(n)?{name:i,not:!1,values:n.map(s=>mh(s))}:{name:i,not:!1,values:[mh(n)]}}):Rt(Object.entries(r??{}),!1)}function Kn(r){return r.values.length!==1?{result:!1,reason:"multiple-values"}:r.values[0].raw!==void 0?{result:!1,reason:"raw-values"}:r.values[0].min!==void 0&&r.values[0].max!==void 0?{result:!1,reason:"min-and-max"}:{result:!0,reason:null}}function hh(r,e,t){let i=Xn(e,r),n=Xn(t,r),s=Kn(i),a=Kn(n);if(s.reason==="multiple-values"||a.reason==="multiple-values")throw new Error("Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.");if(s.reason==="raw-values"||a.reason==="raw-values")throw new Error("Attempted to sort a screen with raw values. This should never happen. Please open a bug report.");if(s.reason==="min-and-max"||a.reason==="min-and-max")throw new Error("Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.");let{min:o,max:l}=i.values[0],{min:c,max:f}=n.values[0];e.not&&([o,l]=[l,o]),t.not&&([c,f]=[f,c]),o=o===void 0?o:parseFloat(o),l=l===void 0?l:parseFloat(l),c=c===void 0?c:parseFloat(c),f=f===void 0?f:parseFloat(f);let[d,p]=r==="min"?[o,c]:[f,l];return d-p}function Xn(r,e){return typeof r=="object"?r:{name:"arbitrary-screen",values:[{[e]:r}]}}function mh({"min-width":r,min:e=r,max:t,raw:i}={}){return{min:e,max:t,raw:i}}var Zn=P(()=>{u()});function Jn(r,e){r.walkDecls(t=>{if(e.includes(t.prop)){t.remove();return}for(let i of e)t.value.includes(`/ var(${i})`)?t.value=t.value.replace(`/ var(${i})`,""):t.value.includes(`/ var(${i}, 1)`)&&(t.value=t.value.replace(`/ var(${i}, 1)`,""))})}var gh=P(()=>{u()});var se,Xe,nt,ge,yh,bh=P(()=>{u();ft();et();Ot();oh();Yn();fr();uh();ch();Lr();ra();Kt();Ci();ph();Be();Zn();Ys();gh();ct();Br();Oi();se={childVariant:({addVariant:r})=>{r("*","& > *")},pseudoElementVariants:({addVariant:r})=>{r("first-letter","&::first-letter"),r("first-line","&::first-line"),r("marker",[({container:e})=>(Jn(e,["--tw-text-opacity"]),"& *::marker"),({container:e})=>(Jn(e,["--tw-text-opacity"]),"&::marker")]),r("selection",["& *::selection","&::selection"]),r("file","&::file-selector-button"),r("placeholder","&::placeholder"),r("backdrop","&::backdrop"),r("before",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(ee.decl({prop:"content",value:"var(--tw-content)"}))}),"&::before")),r("after",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(ee.decl({prop:"content",value:"var(--tw-content)"}))}),"&::after"))},pseudoClassVariants:({addVariant:r,matchVariant:e,config:t,prefix:i})=>{let n=[["first","&:first-child"],["last","&:last-child"],["only","&:only-child"],["odd","&:nth-child(odd)"],["even","&:nth-child(even)"],"first-of-type","last-of-type","only-of-type",["visited",({container:a})=>(Jn(a,["--tw-text-opacity","--tw-border-opacity","--tw-bg-opacity"]),"&:visited")],"target",["open","&[open]"],"default","checked","indeterminate","placeholder-shown","autofill","optional","required","valid","invalid","in-range","out-of-range","read-only","empty","focus-within",["hover",we(t(),"hoverOnlyWhenSupported")?"@media (hover: hover) and (pointer: fine) { &:hover }":"&:hover"],"focus","focus-visible","active","enabled","disabled"].map(a=>Array.isArray(a)?a:[a,`&:${a}`]);for(let[a,o]of n)r(a,l=>typeof o=="function"?o(l):o);let s={group:(a,{modifier:o})=>o?[`:merge(${i(".group")}\\/${Te(o)})`," &"]:[`:merge(${i(".group")})`," &"],peer:(a,{modifier:o})=>o?[`:merge(${i(".peer")}\\/${Te(o)})`," ~ &"]:[`:merge(${i(".peer")})`," ~ &"]};for(let[a,o]of Object.entries(s))e(a,(l="",c)=>{let f=K(typeof l=="function"?l(c):l);f.includes("&")||(f="&"+f);let[d,p]=o("",c),h=null,b=null,v=0;for(let y=0;y{r("ltr",'&:where([dir="ltr"], [dir="ltr"] *)'),r("rtl",'&:where([dir="rtl"], [dir="rtl"] *)')},reducedMotionVariants:({addVariant:r})=>{r("motion-safe","@media (prefers-reduced-motion: no-preference)"),r("motion-reduce","@media (prefers-reduced-motion: reduce)")},darkVariants:({config:r,addVariant:e})=>{let[t,i=".dark"]=[].concat(r("darkMode","media"));if(t===!1&&(t="media",G.warn("darkmode-false",["The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.","Change `darkMode` to `media` or remove it entirely.","https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration"])),t==="variant"){let n;if(Array.isArray(i)||typeof i=="function"?n=i:typeof i=="string"&&(n=[i]),Array.isArray(n))for(let s of n)s===".dark"?(t=!1,G.warn("darkmode-variant-without-selector",["When using `variant` for `darkMode`, you must provide a selector.",'Example: `darkMode: ["variant", ".your-selector &"]`'])):s.includes("&")||(t=!1,G.warn("darkmode-variant-without-ampersand",["When using `variant` for `darkMode`, your selector must contain `&`.",'Example `darkMode: ["variant", ".your-selector &"]`']));i=n}t==="selector"?e("dark",`&:where(${i}, ${i} *)`):t==="media"?e("dark","@media (prefers-color-scheme: dark)"):t==="variant"?e("dark",i):t==="class"&&e("dark",`&:is(${i} *)`)},printVariant:({addVariant:r})=>{r("print","@media print")},screenVariants:({theme:r,addVariant:e,matchVariant:t})=>{let i=r("screens")??{},n=Object.values(i).every(w=>typeof w=="string"),s=Rt(r("screens")),a=new Set([]);function o(w){return w.match(/(\D+)$/)?.[1]??"(none)"}function l(w){w!==void 0&&a.add(o(w))}function c(w){return l(w),a.size===1}for(let w of s)for(let k of w.values)l(k.min),l(k.max);let f=a.size<=1;function d(w){return Object.fromEntries(s.filter(k=>Kn(k).result).map(k=>{let{min:S,max:E}=k.values[0];if(w==="min"&&S!==void 0)return k;if(w==="min"&&E!==void 0)return{...k,not:!k.not};if(w==="max"&&E!==void 0)return k;if(w==="max"&&S!==void 0)return{...k,not:!k.not}}).map(k=>[k.name,k]))}function p(w){return(k,S)=>hh(w,k.value,S.value)}let h=p("max"),b=p("min");function v(w){return k=>{if(n)if(f){if(typeof k=="string"&&!c(k))return G.warn("minmax-have-mixed-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[]}else return G.warn("mixed-screen-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[];else return G.warn("complex-screen-config",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing objects."]),[];return[`@media ${Tt(Xn(k,w))}`]}}t("max",v("max"),{sort:h,values:n?d("max"):{}});let y="min-screens";for(let w of s)e(w.name,`@media ${Tt(w)}`,{id:y,sort:n&&f?b:void 0,value:w});t("min",v("min"),{id:y,sort:b})},supportsVariants:({matchVariant:r,theme:e})=>{r("supports",(t="")=>{let i=K(t),n=/^\w*\s*\(/.test(i);return i=n?i.replace(/\b(and|or|not)\b/g," $1 "):i,n?`@supports ${i}`:(i.includes(":")||(i=`${i}: var(--tw)`),i.startsWith("(")&&i.endsWith(")")||(i=`(${i})`),`@supports ${i}`)},{values:e("supports")??{}})},hasVariants:({matchVariant:r,prefix:e})=>{r("has",t=>`&:has(${K(t)})`,{values:{},[Pt]:{respectPrefix:!1}}),r("group-has",(t,{modifier:i})=>i?`:merge(${e(".group")}\\/${i}):has(${K(t)}) &`:`:merge(${e(".group")}):has(${K(t)}) &`,{values:{},[Pt]:{respectPrefix:!1}}),r("peer-has",(t,{modifier:i})=>i?`:merge(${e(".peer")}\\/${i}):has(${K(t)}) ~ &`:`:merge(${e(".peer")}):has(${K(t)}) ~ &`,{values:{},[Pt]:{respectPrefix:!1}})},ariaVariants:({matchVariant:r,theme:e})=>{r("aria",t=>`&[aria-${Ye(K(t))}]`,{values:e("aria")??{}}),r("group-aria",(t,{modifier:i})=>i?`:merge(.group\\/${i})[aria-${Ye(K(t))}] &`:`:merge(.group)[aria-${Ye(K(t))}] &`,{values:e("aria")??{}}),r("peer-aria",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[aria-${Ye(K(t))}] ~ &`:`:merge(.peer)[aria-${Ye(K(t))}] ~ &`,{values:e("aria")??{}})},dataVariants:({matchVariant:r,theme:e})=>{r("data",t=>`&[data-${Ye(K(t))}]`,{values:e("data")??{}}),r("group-data",(t,{modifier:i})=>i?`:merge(.group\\/${i})[data-${Ye(K(t))}] &`:`:merge(.group)[data-${Ye(K(t))}] &`,{values:e("data")??{}}),r("peer-data",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[data-${Ye(K(t))}] ~ &`:`:merge(.peer)[data-${Ye(K(t))}] ~ &`,{values:e("data")??{}})},orientationVariants:({addVariant:r})=>{r("portrait","@media (orientation: portrait)"),r("landscape","@media (orientation: landscape)")},prefersContrastVariants:({addVariant:r})=>{r("contrast-more","@media (prefers-contrast: more)"),r("contrast-less","@media (prefers-contrast: less)")},forcedColorsVariants:({addVariant:r})=>{r("forced-colors","@media (forced-colors: active)")}},Xe=["translate(var(--tw-translate-x), var(--tw-translate-y))","rotate(var(--tw-rotate))","skewX(var(--tw-skew-x))","skewY(var(--tw-skew-y))","scaleX(var(--tw-scale-x))","scaleY(var(--tw-scale-y))"].join(" "),nt=["var(--tw-blur)","var(--tw-brightness)","var(--tw-contrast)","var(--tw-grayscale)","var(--tw-hue-rotate)","var(--tw-invert)","var(--tw-saturate)","var(--tw-sepia)","var(--tw-drop-shadow)"].join(" "),ge=["var(--tw-backdrop-blur)","var(--tw-backdrop-brightness)","var(--tw-backdrop-contrast)","var(--tw-backdrop-grayscale)","var(--tw-backdrop-hue-rotate)","var(--tw-backdrop-invert)","var(--tw-backdrop-opacity)","var(--tw-backdrop-saturate)","var(--tw-backdrop-sepia)"].join(" "),yh={preflight:({addBase:r})=>{let e=ee.parse(`*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:theme('borderColor.DEFAULT', currentColor)}::after,::before{--tw-content:''}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:theme('fontFamily.sans', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:theme('fontFamily.sans[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.sans[1].fontVariationSettings', normal);-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:theme('fontFamily.mono[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.mono[1].fontVariationSettings', normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:theme('colors.gray.4', #9ca3af)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}`);r([ee.comment({text:`! tailwindcss v${dh} | MIT License | https://tailwindcss.com`}),...e.nodes])},container:(()=>{function r(t=[]){return t.flatMap(i=>i.values.map(n=>n.min)).filter(i=>i!==void 0)}function e(t,i,n){if(typeof n=="undefined")return[];if(!(typeof n=="object"&&n!==null))return[{screen:"DEFAULT",minWidth:0,padding:n}];let s=[];n.DEFAULT&&s.push({screen:"DEFAULT",minWidth:0,padding:n.DEFAULT});for(let a of t)for(let o of i)for(let{min:l}of o.values)l===a&&s.push({minWidth:a,padding:n[o.name]});return s}return function({addComponents:t,theme:i}){let n=Rt(i("container.screens",i("screens"))),s=r(n),a=e(s,n,i("container.padding")),o=c=>{let f=a.find(d=>d.minWidth===c);return f?{paddingRight:f.padding,paddingLeft:f.padding}:{}},l=Array.from(new Set(s.slice().sort((c,f)=>parseInt(c)-parseInt(f)))).map(c=>({[`@media (min-width: ${c})`]:{".container":{"max-width":c,...o(c)}}}));t([{".container":Object.assign({width:"100%"},i("container.center",!1)?{marginRight:"auto",marginLeft:"auto"}:{},o(0))},...l])}})(),accessibility:({addUtilities:r})=>{r({".sr-only":{position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"},".not-sr-only":{position:"static",width:"auto",height:"auto",padding:"0",margin:"0",overflow:"visible",clip:"auto",whiteSpace:"normal"}})},pointerEvents:({addUtilities:r})=>{r({".pointer-events-none":{"pointer-events":"none"},".pointer-events-auto":{"pointer-events":"auto"}})},visibility:({addUtilities:r})=>{r({".visible":{visibility:"visible"},".invisible":{visibility:"hidden"},".collapse":{visibility:"collapse"}})},position:({addUtilities:r})=>{r({".static":{position:"static"},".fixed":{position:"fixed"},".absolute":{position:"absolute"},".relative":{position:"relative"},".sticky":{position:"sticky"}})},inset:L("inset",[["inset",["inset"]],[["inset-x",["left","right"]],["inset-y",["top","bottom"]]],[["start",["inset-inline-start"]],["end",["inset-inline-end"]],["top",["top"]],["right",["right"]],["bottom",["bottom"]],["left",["left"]]]],{supportsNegativeValues:!0}),isolation:({addUtilities:r})=>{r({".isolate":{isolation:"isolate"},".isolation-auto":{isolation:"auto"}})},zIndex:L("zIndex",[["z",["zIndex"]]],{supportsNegativeValues:!0}),order:L("order",void 0,{supportsNegativeValues:!0}),gridColumn:L("gridColumn",[["col",["gridColumn"]]]),gridColumnStart:L("gridColumnStart",[["col-start",["gridColumnStart"]]],{supportsNegativeValues:!0}),gridColumnEnd:L("gridColumnEnd",[["col-end",["gridColumnEnd"]]],{supportsNegativeValues:!0}),gridRow:L("gridRow",[["row",["gridRow"]]]),gridRowStart:L("gridRowStart",[["row-start",["gridRowStart"]]],{supportsNegativeValues:!0}),gridRowEnd:L("gridRowEnd",[["row-end",["gridRowEnd"]]],{supportsNegativeValues:!0}),float:({addUtilities:r})=>{r({".float-start":{float:"inline-start"},".float-end":{float:"inline-end"},".float-right":{float:"right"},".float-left":{float:"left"},".float-none":{float:"none"}})},clear:({addUtilities:r})=>{r({".clear-start":{clear:"inline-start"},".clear-end":{clear:"inline-end"},".clear-left":{clear:"left"},".clear-right":{clear:"right"},".clear-both":{clear:"both"},".clear-none":{clear:"none"}})},margin:L("margin",[["m",["margin"]],[["mx",["margin-left","margin-right"]],["my",["margin-top","margin-bottom"]]],[["ms",["margin-inline-start"]],["me",["margin-inline-end"]],["mt",["margin-top"]],["mr",["margin-right"]],["mb",["margin-bottom"]],["ml",["margin-left"]]]],{supportsNegativeValues:!0}),boxSizing:({addUtilities:r})=>{r({".box-border":{"box-sizing":"border-box"},".box-content":{"box-sizing":"content-box"}})},lineClamp:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"line-clamp":i=>({overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":`${i}`})},{values:t("lineClamp")}),e({".line-clamp-none":{overflow:"visible",display:"block","-webkit-box-orient":"horizontal","-webkit-line-clamp":"none"}})},display:({addUtilities:r})=>{r({".block":{display:"block"},".inline-block":{display:"inline-block"},".inline":{display:"inline"},".flex":{display:"flex"},".inline-flex":{display:"inline-flex"},".table":{display:"table"},".inline-table":{display:"inline-table"},".table-caption":{display:"table-caption"},".table-cell":{display:"table-cell"},".table-column":{display:"table-column"},".table-column-group":{display:"table-column-group"},".table-footer-group":{display:"table-footer-group"},".table-header-group":{display:"table-header-group"},".table-row-group":{display:"table-row-group"},".table-row":{display:"table-row"},".flow-root":{display:"flow-root"},".grid":{display:"grid"},".inline-grid":{display:"inline-grid"},".contents":{display:"contents"},".list-item":{display:"list-item"},".hidden":{display:"none"}})},aspectRatio:L("aspectRatio",[["aspect",["aspect-ratio"]]]),size:L("size",[["size",["width","height"]]]),height:L("height",[["h",["height"]]]),maxHeight:L("maxHeight",[["max-h",["maxHeight"]]]),minHeight:L("minHeight",[["min-h",["minHeight"]]]),width:L("width",[["w",["width"]]]),minWidth:L("minWidth",[["min-w",["minWidth"]]]),maxWidth:L("maxWidth",[["max-w",["maxWidth"]]]),flex:L("flex"),flexShrink:L("flexShrink",[["flex-shrink",["flex-shrink"]],["shrink",["flex-shrink"]]]),flexGrow:L("flexGrow",[["flex-grow",["flex-grow"]],["grow",["flex-grow"]]]),flexBasis:L("flexBasis",[["basis",["flex-basis"]]]),tableLayout:({addUtilities:r})=>{r({".table-auto":{"table-layout":"auto"},".table-fixed":{"table-layout":"fixed"}})},captionSide:({addUtilities:r})=>{r({".caption-top":{"caption-side":"top"},".caption-bottom":{"caption-side":"bottom"}})},borderCollapse:({addUtilities:r})=>{r({".border-collapse":{"border-collapse":"collapse"},".border-separate":{"border-collapse":"separate"}})},borderSpacing:({addDefaults:r,matchUtilities:e,theme:t})=>{r("border-spacing",{"--tw-border-spacing-x":0,"--tw-border-spacing-y":0}),e({"border-spacing":i=>({"--tw-border-spacing-x":i,"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-x":i=>({"--tw-border-spacing-x":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-y":i=>({"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"})},{values:t("borderSpacing")})},transformOrigin:L("transformOrigin",[["origin",["transformOrigin"]]]),translate:L("translate",[[["translate-x",[["@defaults transform",{}],"--tw-translate-x",["transform",Xe]]],["translate-y",[["@defaults transform",{}],"--tw-translate-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),rotate:L("rotate",[["rotate",[["@defaults transform",{}],"--tw-rotate",["transform",Xe]]]],{supportsNegativeValues:!0}),skew:L("skew",[[["skew-x",[["@defaults transform",{}],"--tw-skew-x",["transform",Xe]]],["skew-y",[["@defaults transform",{}],"--tw-skew-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),scale:L("scale",[["scale",[["@defaults transform",{}],"--tw-scale-x","--tw-scale-y",["transform",Xe]]],[["scale-x",[["@defaults transform",{}],"--tw-scale-x",["transform",Xe]]],["scale-y",[["@defaults transform",{}],"--tw-scale-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),transform:({addDefaults:r,addUtilities:e})=>{r("transform",{"--tw-translate-x":"0","--tw-translate-y":"0","--tw-rotate":"0","--tw-skew-x":"0","--tw-skew-y":"0","--tw-scale-x":"1","--tw-scale-y":"1"}),e({".transform":{"@defaults transform":{},transform:Xe},".transform-cpu":{transform:Xe},".transform-gpu":{transform:Xe.replace("translate(var(--tw-translate-x), var(--tw-translate-y))","translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)")},".transform-none":{transform:"none"}})},animation:({matchUtilities:r,theme:e,config:t})=>{let i=s=>Te(t("prefix")+s),n=Object.fromEntries(Object.entries(e("keyframes")??{}).map(([s,a])=>[s,{[`@keyframes ${i(s)}`]:a}]));r({animate:s=>{let a=Mo(s);return[...a.flatMap(o=>n[o.name]),{animation:a.map(({name:o,value:l})=>o===void 0||n[o]===void 0?l:l.replace(o,i(o))).join(", ")}]}},{values:e("animation")})},cursor:L("cursor"),touchAction:({addDefaults:r,addUtilities:e})=>{r("touch-action",{"--tw-pan-x":" ","--tw-pan-y":" ","--tw-pinch-zoom":" "});let t="var(--tw-pan-x) var(--tw-pan-y) var(--tw-pinch-zoom)";e({".touch-auto":{"touch-action":"auto"},".touch-none":{"touch-action":"none"},".touch-pan-x":{"@defaults touch-action":{},"--tw-pan-x":"pan-x","touch-action":t},".touch-pan-left":{"@defaults touch-action":{},"--tw-pan-x":"pan-left","touch-action":t},".touch-pan-right":{"@defaults touch-action":{},"--tw-pan-x":"pan-right","touch-action":t},".touch-pan-y":{"@defaults touch-action":{},"--tw-pan-y":"pan-y","touch-action":t},".touch-pan-up":{"@defaults touch-action":{},"--tw-pan-y":"pan-up","touch-action":t},".touch-pan-down":{"@defaults touch-action":{},"--tw-pan-y":"pan-down","touch-action":t},".touch-pinch-zoom":{"@defaults touch-action":{},"--tw-pinch-zoom":"pinch-zoom","touch-action":t},".touch-manipulation":{"touch-action":"manipulation"}})},userSelect:({addUtilities:r})=>{r({".select-none":{"user-select":"none"},".select-text":{"user-select":"text"},".select-all":{"user-select":"all"},".select-auto":{"user-select":"auto"}})},resize:({addUtilities:r})=>{r({".resize-none":{resize:"none"},".resize-y":{resize:"vertical"},".resize-x":{resize:"horizontal"},".resize":{resize:"both"}})},scrollSnapType:({addDefaults:r,addUtilities:e})=>{r("scroll-snap-type",{"--tw-scroll-snap-strictness":"proximity"}),e({".snap-none":{"scroll-snap-type":"none"},".snap-x":{"@defaults scroll-snap-type":{},"scroll-snap-type":"x var(--tw-scroll-snap-strictness)"},".snap-y":{"@defaults scroll-snap-type":{},"scroll-snap-type":"y var(--tw-scroll-snap-strictness)"},".snap-both":{"@defaults scroll-snap-type":{},"scroll-snap-type":"both var(--tw-scroll-snap-strictness)"},".snap-mandatory":{"--tw-scroll-snap-strictness":"mandatory"},".snap-proximity":{"--tw-scroll-snap-strictness":"proximity"}})},scrollSnapAlign:({addUtilities:r})=>{r({".snap-start":{"scroll-snap-align":"start"},".snap-end":{"scroll-snap-align":"end"},".snap-center":{"scroll-snap-align":"center"},".snap-align-none":{"scroll-snap-align":"none"}})},scrollSnapStop:({addUtilities:r})=>{r({".snap-normal":{"scroll-snap-stop":"normal"},".snap-always":{"scroll-snap-stop":"always"}})},scrollMargin:L("scrollMargin",[["scroll-m",["scroll-margin"]],[["scroll-mx",["scroll-margin-left","scroll-margin-right"]],["scroll-my",["scroll-margin-top","scroll-margin-bottom"]]],[["scroll-ms",["scroll-margin-inline-start"]],["scroll-me",["scroll-margin-inline-end"]],["scroll-mt",["scroll-margin-top"]],["scroll-mr",["scroll-margin-right"]],["scroll-mb",["scroll-margin-bottom"]],["scroll-ml",["scroll-margin-left"]]]],{supportsNegativeValues:!0}),scrollPadding:L("scrollPadding",[["scroll-p",["scroll-padding"]],[["scroll-px",["scroll-padding-left","scroll-padding-right"]],["scroll-py",["scroll-padding-top","scroll-padding-bottom"]]],[["scroll-ps",["scroll-padding-inline-start"]],["scroll-pe",["scroll-padding-inline-end"]],["scroll-pt",["scroll-padding-top"]],["scroll-pr",["scroll-padding-right"]],["scroll-pb",["scroll-padding-bottom"]],["scroll-pl",["scroll-padding-left"]]]]),listStylePosition:({addUtilities:r})=>{r({".list-inside":{"list-style-position":"inside"},".list-outside":{"list-style-position":"outside"}})},listStyleType:L("listStyleType",[["list",["listStyleType"]]]),listStyleImage:L("listStyleImage",[["list-image",["listStyleImage"]]]),appearance:({addUtilities:r})=>{r({".appearance-none":{appearance:"none"},".appearance-auto":{appearance:"auto"}})},columns:L("columns",[["columns",["columns"]]]),breakBefore:({addUtilities:r})=>{r({".break-before-auto":{"break-before":"auto"},".break-before-avoid":{"break-before":"avoid"},".break-before-all":{"break-before":"all"},".break-before-avoid-page":{"break-before":"avoid-page"},".break-before-page":{"break-before":"page"},".break-before-left":{"break-before":"left"},".break-before-right":{"break-before":"right"},".break-before-column":{"break-before":"column"}})},breakInside:({addUtilities:r})=>{r({".break-inside-auto":{"break-inside":"auto"},".break-inside-avoid":{"break-inside":"avoid"},".break-inside-avoid-page":{"break-inside":"avoid-page"},".break-inside-avoid-column":{"break-inside":"avoid-column"}})},breakAfter:({addUtilities:r})=>{r({".break-after-auto":{"break-after":"auto"},".break-after-avoid":{"break-after":"avoid"},".break-after-all":{"break-after":"all"},".break-after-avoid-page":{"break-after":"avoid-page"},".break-after-page":{"break-after":"page"},".break-after-left":{"break-after":"left"},".break-after-right":{"break-after":"right"},".break-after-column":{"break-after":"column"}})},gridAutoColumns:L("gridAutoColumns",[["auto-cols",["gridAutoColumns"]]]),gridAutoFlow:({addUtilities:r})=>{r({".grid-flow-row":{gridAutoFlow:"row"},".grid-flow-col":{gridAutoFlow:"column"},".grid-flow-dense":{gridAutoFlow:"dense"},".grid-flow-row-dense":{gridAutoFlow:"row dense"},".grid-flow-col-dense":{gridAutoFlow:"column dense"}})},gridAutoRows:L("gridAutoRows",[["auto-rows",["gridAutoRows"]]]),gridTemplateColumns:L("gridTemplateColumns",[["grid-cols",["gridTemplateColumns"]]]),gridTemplateRows:L("gridTemplateRows",[["grid-rows",["gridTemplateRows"]]]),flexDirection:({addUtilities:r})=>{r({".flex-row":{"flex-direction":"row"},".flex-row-reverse":{"flex-direction":"row-reverse"},".flex-col":{"flex-direction":"column"},".flex-col-reverse":{"flex-direction":"column-reverse"}})},flexWrap:({addUtilities:r})=>{r({".flex-wrap":{"flex-wrap":"wrap"},".flex-wrap-reverse":{"flex-wrap":"wrap-reverse"},".flex-nowrap":{"flex-wrap":"nowrap"}})},placeContent:({addUtilities:r})=>{r({".place-content-center":{"place-content":"center"},".place-content-start":{"place-content":"start"},".place-content-end":{"place-content":"end"},".place-content-between":{"place-content":"space-between"},".place-content-around":{"place-content":"space-around"},".place-content-evenly":{"place-content":"space-evenly"},".place-content-baseline":{"place-content":"baseline"},".place-content-stretch":{"place-content":"stretch"}})},placeItems:({addUtilities:r})=>{r({".place-items-start":{"place-items":"start"},".place-items-end":{"place-items":"end"},".place-items-center":{"place-items":"center"},".place-items-baseline":{"place-items":"baseline"},".place-items-stretch":{"place-items":"stretch"}})},alignContent:({addUtilities:r})=>{r({".content-normal":{"align-content":"normal"},".content-center":{"align-content":"center"},".content-start":{"align-content":"flex-start"},".content-end":{"align-content":"flex-end"},".content-between":{"align-content":"space-between"},".content-around":{"align-content":"space-around"},".content-evenly":{"align-content":"space-evenly"},".content-baseline":{"align-content":"baseline"},".content-stretch":{"align-content":"stretch"}})},alignItems:({addUtilities:r})=>{r({".items-start":{"align-items":"flex-start"},".items-end":{"align-items":"flex-end"},".items-center":{"align-items":"center"},".items-baseline":{"align-items":"baseline"},".items-stretch":{"align-items":"stretch"}})},justifyContent:({addUtilities:r})=>{r({".justify-normal":{"justify-content":"normal"},".justify-start":{"justify-content":"flex-start"},".justify-end":{"justify-content":"flex-end"},".justify-center":{"justify-content":"center"},".justify-between":{"justify-content":"space-between"},".justify-around":{"justify-content":"space-around"},".justify-evenly":{"justify-content":"space-evenly"},".justify-stretch":{"justify-content":"stretch"}})},justifyItems:({addUtilities:r})=>{r({".justify-items-start":{"justify-items":"start"},".justify-items-end":{"justify-items":"end"},".justify-items-center":{"justify-items":"center"},".justify-items-stretch":{"justify-items":"stretch"}})},gap:L("gap",[["gap",["gap"]],[["gap-x",["columnGap"]],["gap-y",["rowGap"]]]]),space:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"space-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"0","margin-right":`calc(${i} * var(--tw-space-x-reverse))`,"margin-left":`calc(${i} * calc(1 - var(--tw-space-x-reverse)))`}}),"space-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"0","margin-top":`calc(${i} * calc(1 - var(--tw-space-y-reverse)))`,"margin-bottom":`calc(${i} * var(--tw-space-y-reverse))`}})},{values:t("space"),supportsNegativeValues:!0}),e({".space-y-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"1"},".space-x-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"1"}})},divideWidth:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"divide-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"0","border-right-width":`calc(${i} * var(--tw-divide-x-reverse))`,"border-left-width":`calc(${i} * calc(1 - var(--tw-divide-x-reverse)))`}}),"divide-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"0","border-top-width":`calc(${i} * calc(1 - var(--tw-divide-y-reverse)))`,"border-bottom-width":`calc(${i} * var(--tw-divide-y-reverse))`}})},{values:t("divideWidth"),type:["line-width","length","any"]}),e({".divide-y-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"1"},".divide-x-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"1"}})},divideStyle:({addUtilities:r})=>{r({".divide-solid > :not([hidden]) ~ :not([hidden])":{"border-style":"solid"},".divide-dashed > :not([hidden]) ~ :not([hidden])":{"border-style":"dashed"},".divide-dotted > :not([hidden]) ~ :not([hidden])":{"border-style":"dotted"},".divide-double > :not([hidden]) ~ :not([hidden])":{"border-style":"double"},".divide-none > :not([hidden]) ~ :not([hidden])":{"border-style":"none"}})},divideColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({divide:i=>t("divideOpacity")?{["& > :not([hidden]) ~ :not([hidden])"]:Ae({color:i,property:"border-color",variable:"--tw-divide-opacity"})}:{["& > :not([hidden]) ~ :not([hidden])"]:{"border-color":X(i)}}},{values:(({DEFAULT:i,...n})=>n)(xe(e("divideColor"))),type:["color","any"]})},divideOpacity:({matchUtilities:r,theme:e})=>{r({"divide-opacity":t=>({["& > :not([hidden]) ~ :not([hidden])"]:{"--tw-divide-opacity":t}})},{values:e("divideOpacity")})},placeSelf:({addUtilities:r})=>{r({".place-self-auto":{"place-self":"auto"},".place-self-start":{"place-self":"start"},".place-self-end":{"place-self":"end"},".place-self-center":{"place-self":"center"},".place-self-stretch":{"place-self":"stretch"}})},alignSelf:({addUtilities:r})=>{r({".self-auto":{"align-self":"auto"},".self-start":{"align-self":"flex-start"},".self-end":{"align-self":"flex-end"},".self-center":{"align-self":"center"},".self-stretch":{"align-self":"stretch"},".self-baseline":{"align-self":"baseline"}})},justifySelf:({addUtilities:r})=>{r({".justify-self-auto":{"justify-self":"auto"},".justify-self-start":{"justify-self":"start"},".justify-self-end":{"justify-self":"end"},".justify-self-center":{"justify-self":"center"},".justify-self-stretch":{"justify-self":"stretch"}})},overflow:({addUtilities:r})=>{r({".overflow-auto":{overflow:"auto"},".overflow-hidden":{overflow:"hidden"},".overflow-clip":{overflow:"clip"},".overflow-visible":{overflow:"visible"},".overflow-scroll":{overflow:"scroll"},".overflow-x-auto":{"overflow-x":"auto"},".overflow-y-auto":{"overflow-y":"auto"},".overflow-x-hidden":{"overflow-x":"hidden"},".overflow-y-hidden":{"overflow-y":"hidden"},".overflow-x-clip":{"overflow-x":"clip"},".overflow-y-clip":{"overflow-y":"clip"},".overflow-x-visible":{"overflow-x":"visible"},".overflow-y-visible":{"overflow-y":"visible"},".overflow-x-scroll":{"overflow-x":"scroll"},".overflow-y-scroll":{"overflow-y":"scroll"}})},overscrollBehavior:({addUtilities:r})=>{r({".overscroll-auto":{"overscroll-behavior":"auto"},".overscroll-contain":{"overscroll-behavior":"contain"},".overscroll-none":{"overscroll-behavior":"none"},".overscroll-y-auto":{"overscroll-behavior-y":"auto"},".overscroll-y-contain":{"overscroll-behavior-y":"contain"},".overscroll-y-none":{"overscroll-behavior-y":"none"},".overscroll-x-auto":{"overscroll-behavior-x":"auto"},".overscroll-x-contain":{"overscroll-behavior-x":"contain"},".overscroll-x-none":{"overscroll-behavior-x":"none"}})},scrollBehavior:({addUtilities:r})=>{r({".scroll-auto":{"scroll-behavior":"auto"},".scroll-smooth":{"scroll-behavior":"smooth"}})},textOverflow:({addUtilities:r})=>{r({".truncate":{overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"},".overflow-ellipsis":{"text-overflow":"ellipsis"},".text-ellipsis":{"text-overflow":"ellipsis"},".text-clip":{"text-overflow":"clip"}})},hyphens:({addUtilities:r})=>{r({".hyphens-none":{hyphens:"none"},".hyphens-manual":{hyphens:"manual"},".hyphens-auto":{hyphens:"auto"}})},whitespace:({addUtilities:r})=>{r({".whitespace-normal":{"white-space":"normal"},".whitespace-nowrap":{"white-space":"nowrap"},".whitespace-pre":{"white-space":"pre"},".whitespace-pre-line":{"white-space":"pre-line"},".whitespace-pre-wrap":{"white-space":"pre-wrap"},".whitespace-break-spaces":{"white-space":"break-spaces"}})},textWrap:({addUtilities:r})=>{r({".text-wrap":{"text-wrap":"wrap"},".text-nowrap":{"text-wrap":"nowrap"},".text-balance":{"text-wrap":"balance"},".text-pretty":{"text-wrap":"pretty"}})},wordBreak:({addUtilities:r})=>{r({".break-normal":{"overflow-wrap":"normal","word-break":"normal"},".break-words":{"overflow-wrap":"break-word"},".break-all":{"word-break":"break-all"},".break-keep":{"word-break":"keep-all"}})},borderRadius:L("borderRadius",[["rounded",["border-radius"]],[["rounded-s",["border-start-start-radius","border-end-start-radius"]],["rounded-e",["border-start-end-radius","border-end-end-radius"]],["rounded-t",["border-top-left-radius","border-top-right-radius"]],["rounded-r",["border-top-right-radius","border-bottom-right-radius"]],["rounded-b",["border-bottom-right-radius","border-bottom-left-radius"]],["rounded-l",["border-top-left-radius","border-bottom-left-radius"]]],[["rounded-ss",["border-start-start-radius"]],["rounded-se",["border-start-end-radius"]],["rounded-ee",["border-end-end-radius"]],["rounded-es",["border-end-start-radius"]],["rounded-tl",["border-top-left-radius"]],["rounded-tr",["border-top-right-radius"]],["rounded-br",["border-bottom-right-radius"]],["rounded-bl",["border-bottom-left-radius"]]]]),borderWidth:L("borderWidth",[["border",[["@defaults border-width",{}],"border-width"]],[["border-x",[["@defaults border-width",{}],"border-left-width","border-right-width"]],["border-y",[["@defaults border-width",{}],"border-top-width","border-bottom-width"]]],[["border-s",[["@defaults border-width",{}],"border-inline-start-width"]],["border-e",[["@defaults border-width",{}],"border-inline-end-width"]],["border-t",[["@defaults border-width",{}],"border-top-width"]],["border-r",[["@defaults border-width",{}],"border-right-width"]],["border-b",[["@defaults border-width",{}],"border-bottom-width"]],["border-l",[["@defaults border-width",{}],"border-left-width"]]]],{type:["line-width","length"]}),borderStyle:({addUtilities:r})=>{r({".border-solid":{"border-style":"solid"},".border-dashed":{"border-style":"dashed"},".border-dotted":{"border-style":"dotted"},".border-double":{"border-style":"double"},".border-hidden":{"border-style":"hidden"},".border-none":{"border-style":"none"}})},borderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({border:i=>t("borderOpacity")?Ae({color:i,property:"border-color",variable:"--tw-border-opacity"}):{"border-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]}),r({"border-x":i=>t("borderOpacity")?Ae({color:i,property:["border-left-color","border-right-color"],variable:"--tw-border-opacity"}):{"border-left-color":X(i),"border-right-color":X(i)},"border-y":i=>t("borderOpacity")?Ae({color:i,property:["border-top-color","border-bottom-color"],variable:"--tw-border-opacity"}):{"border-top-color":X(i),"border-bottom-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]}),r({"border-s":i=>t("borderOpacity")?Ae({color:i,property:"border-inline-start-color",variable:"--tw-border-opacity"}):{"border-inline-start-color":X(i)},"border-e":i=>t("borderOpacity")?Ae({color:i,property:"border-inline-end-color",variable:"--tw-border-opacity"}):{"border-inline-end-color":X(i)},"border-t":i=>t("borderOpacity")?Ae({color:i,property:"border-top-color",variable:"--tw-border-opacity"}):{"border-top-color":X(i)},"border-r":i=>t("borderOpacity")?Ae({color:i,property:"border-right-color",variable:"--tw-border-opacity"}):{"border-right-color":X(i)},"border-b":i=>t("borderOpacity")?Ae({color:i,property:"border-bottom-color",variable:"--tw-border-opacity"}):{"border-bottom-color":X(i)},"border-l":i=>t("borderOpacity")?Ae({color:i,property:"border-left-color",variable:"--tw-border-opacity"}):{"border-left-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]})},borderOpacity:L("borderOpacity",[["border-opacity",["--tw-border-opacity"]]]),backgroundColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({bg:i=>t("backgroundOpacity")?Ae({color:i,property:"background-color",variable:"--tw-bg-opacity"}):{"background-color":X(i)}},{values:xe(e("backgroundColor")),type:["color","any"]})},backgroundOpacity:L("backgroundOpacity",[["bg-opacity",["--tw-bg-opacity"]]]),backgroundImage:L("backgroundImage",[["bg",["background-image"]]],{type:["lookup","image","url"]}),gradientColorStops:(()=>{function r(e){return Je(e,0,"rgb(255 255 255 / 0)")}return function({matchUtilities:e,theme:t,addDefaults:i}){i("gradient-color-stops",{"--tw-gradient-from-position":" ","--tw-gradient-via-position":" ","--tw-gradient-to-position":" "});let n={values:xe(t("gradientColorStops")),type:["color","any"]},s={values:t("gradientColorStopPositions"),type:["length","percentage"]};e({from:a=>{let o=r(a);return{"@defaults gradient-color-stops":{},"--tw-gradient-from":`${X(a)} var(--tw-gradient-from-position)`,"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":"var(--tw-gradient-from), var(--tw-gradient-to)"}}},n),e({from:a=>({"--tw-gradient-from-position":a})},s),e({via:a=>{let o=r(a);return{"@defaults gradient-color-stops":{},"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":`var(--tw-gradient-from), ${X(a)} var(--tw-gradient-via-position), var(--tw-gradient-to)`}}},n),e({via:a=>({"--tw-gradient-via-position":a})},s),e({to:a=>({"@defaults gradient-color-stops":{},"--tw-gradient-to":`${X(a)} var(--tw-gradient-to-position)`})},n),e({to:a=>({"--tw-gradient-to-position":a})},s)}})(),boxDecorationBreak:({addUtilities:r})=>{r({".decoration-slice":{"box-decoration-break":"slice"},".decoration-clone":{"box-decoration-break":"clone"},".box-decoration-slice":{"box-decoration-break":"slice"},".box-decoration-clone":{"box-decoration-break":"clone"}})},backgroundSize:L("backgroundSize",[["bg",["background-size"]]],{type:["lookup","length","percentage","size"]}),backgroundAttachment:({addUtilities:r})=>{r({".bg-fixed":{"background-attachment":"fixed"},".bg-local":{"background-attachment":"local"},".bg-scroll":{"background-attachment":"scroll"}})},backgroundClip:({addUtilities:r})=>{r({".bg-clip-border":{"background-clip":"border-box"},".bg-clip-padding":{"background-clip":"padding-box"},".bg-clip-content":{"background-clip":"content-box"},".bg-clip-text":{"background-clip":"text"}})},backgroundPosition:L("backgroundPosition",[["bg",["background-position"]]],{type:["lookup",["position",{preferOnConflict:!0}]]}),backgroundRepeat:({addUtilities:r})=>{r({".bg-repeat":{"background-repeat":"repeat"},".bg-no-repeat":{"background-repeat":"no-repeat"},".bg-repeat-x":{"background-repeat":"repeat-x"},".bg-repeat-y":{"background-repeat":"repeat-y"},".bg-repeat-round":{"background-repeat":"round"},".bg-repeat-space":{"background-repeat":"space"}})},backgroundOrigin:({addUtilities:r})=>{r({".bg-origin-border":{"background-origin":"border-box"},".bg-origin-padding":{"background-origin":"padding-box"},".bg-origin-content":{"background-origin":"content-box"}})},fill:({matchUtilities:r,theme:e})=>{r({fill:t=>({fill:X(t)})},{values:xe(e("fill")),type:["color","any"]})},stroke:({matchUtilities:r,theme:e})=>{r({stroke:t=>({stroke:X(t)})},{values:xe(e("stroke")),type:["color","url","any"]})},strokeWidth:L("strokeWidth",[["stroke",["stroke-width"]]],{type:["length","number","percentage"]}),objectFit:({addUtilities:r})=>{r({".object-contain":{"object-fit":"contain"},".object-cover":{"object-fit":"cover"},".object-fill":{"object-fit":"fill"},".object-none":{"object-fit":"none"},".object-scale-down":{"object-fit":"scale-down"}})},objectPosition:L("objectPosition",[["object",["object-position"]]]),padding:L("padding",[["p",["padding"]],[["px",["padding-left","padding-right"]],["py",["padding-top","padding-bottom"]]],[["ps",["padding-inline-start"]],["pe",["padding-inline-end"]],["pt",["padding-top"]],["pr",["padding-right"]],["pb",["padding-bottom"]],["pl",["padding-left"]]]]),textAlign:({addUtilities:r})=>{r({".text-left":{"text-align":"left"},".text-center":{"text-align":"center"},".text-right":{"text-align":"right"},".text-justify":{"text-align":"justify"},".text-start":{"text-align":"start"},".text-end":{"text-align":"end"}})},textIndent:L("textIndent",[["indent",["text-indent"]]],{supportsNegativeValues:!0}),verticalAlign:({addUtilities:r,matchUtilities:e})=>{r({".align-baseline":{"vertical-align":"baseline"},".align-top":{"vertical-align":"top"},".align-middle":{"vertical-align":"middle"},".align-bottom":{"vertical-align":"bottom"},".align-text-top":{"vertical-align":"text-top"},".align-text-bottom":{"vertical-align":"text-bottom"},".align-sub":{"vertical-align":"sub"},".align-super":{"vertical-align":"super"}}),e({align:t=>({"vertical-align":t})})},fontFamily:({matchUtilities:r,theme:e})=>{r({font:t=>{let[i,n={}]=Array.isArray(t)&&ke(t[1])?t:[t],{fontFeatureSettings:s,fontVariationSettings:a}=n;return{"font-family":Array.isArray(i)?i.join(", "):i,...s===void 0?{}:{"font-feature-settings":s},...a===void 0?{}:{"font-variation-settings":a}}}},{values:e("fontFamily"),type:["lookup","generic-name","family-name"]})},fontSize:({matchUtilities:r,theme:e})=>{r({text:(t,{modifier:i})=>{let[n,s]=Array.isArray(t)?t:[t];if(i)return{"font-size":n,"line-height":i};let{lineHeight:a,letterSpacing:o,fontWeight:l}=ke(s)?s:{lineHeight:s};return{"font-size":n,...a===void 0?{}:{"line-height":a},...o===void 0?{}:{"letter-spacing":o},...l===void 0?{}:{"font-weight":l}}}},{values:e("fontSize"),modifiers:e("lineHeight"),type:["absolute-size","relative-size","length","percentage"]})},fontWeight:L("fontWeight",[["font",["fontWeight"]]],{type:["lookup","number","any"]}),textTransform:({addUtilities:r})=>{r({".uppercase":{"text-transform":"uppercase"},".lowercase":{"text-transform":"lowercase"},".capitalize":{"text-transform":"capitalize"},".normal-case":{"text-transform":"none"}})},fontStyle:({addUtilities:r})=>{r({".italic":{"font-style":"italic"},".not-italic":{"font-style":"normal"}})},fontVariantNumeric:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)";r("font-variant-numeric",{"--tw-ordinal":" ","--tw-slashed-zero":" ","--tw-numeric-figure":" ","--tw-numeric-spacing":" ","--tw-numeric-fraction":" "}),e({".normal-nums":{"font-variant-numeric":"normal"},".ordinal":{"@defaults font-variant-numeric":{},"--tw-ordinal":"ordinal","font-variant-numeric":t},".slashed-zero":{"@defaults font-variant-numeric":{},"--tw-slashed-zero":"slashed-zero","font-variant-numeric":t},".lining-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"lining-nums","font-variant-numeric":t},".oldstyle-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"oldstyle-nums","font-variant-numeric":t},".proportional-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"proportional-nums","font-variant-numeric":t},".tabular-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"tabular-nums","font-variant-numeric":t},".diagonal-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"diagonal-fractions","font-variant-numeric":t},".stacked-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"stacked-fractions","font-variant-numeric":t}})},lineHeight:L("lineHeight",[["leading",["lineHeight"]]]),letterSpacing:L("letterSpacing",[["tracking",["letterSpacing"]]],{supportsNegativeValues:!0}),textColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({text:i=>t("textOpacity")?Ae({color:i,property:"color",variable:"--tw-text-opacity"}):{color:X(i)}},{values:xe(e("textColor")),type:["color","any"]})},textOpacity:L("textOpacity",[["text-opacity",["--tw-text-opacity"]]]),textDecoration:({addUtilities:r})=>{r({".underline":{"text-decoration-line":"underline"},".overline":{"text-decoration-line":"overline"},".line-through":{"text-decoration-line":"line-through"},".no-underline":{"text-decoration-line":"none"}})},textDecorationColor:({matchUtilities:r,theme:e})=>{r({decoration:t=>({"text-decoration-color":X(t)})},{values:xe(e("textDecorationColor")),type:["color","any"]})},textDecorationStyle:({addUtilities:r})=>{r({".decoration-solid":{"text-decoration-style":"solid"},".decoration-double":{"text-decoration-style":"double"},".decoration-dotted":{"text-decoration-style":"dotted"},".decoration-dashed":{"text-decoration-style":"dashed"},".decoration-wavy":{"text-decoration-style":"wavy"}})},textDecorationThickness:L("textDecorationThickness",[["decoration",["text-decoration-thickness"]]],{type:["length","percentage"]}),textUnderlineOffset:L("textUnderlineOffset",[["underline-offset",["text-underline-offset"]]],{type:["length","percentage","any"]}),fontSmoothing:({addUtilities:r})=>{r({".antialiased":{"-webkit-font-smoothing":"antialiased","-moz-osx-font-smoothing":"grayscale"},".subpixel-antialiased":{"-webkit-font-smoothing":"auto","-moz-osx-font-smoothing":"auto"}})},placeholderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({placeholder:i=>t("placeholderOpacity")?{"&::placeholder":Ae({color:i,property:"color",variable:"--tw-placeholder-opacity"})}:{"&::placeholder":{color:X(i)}}},{values:xe(e("placeholderColor")),type:["color","any"]})},placeholderOpacity:({matchUtilities:r,theme:e})=>{r({"placeholder-opacity":t=>({["&::placeholder"]:{"--tw-placeholder-opacity":t}})},{values:e("placeholderOpacity")})},caretColor:({matchUtilities:r,theme:e})=>{r({caret:t=>({"caret-color":X(t)})},{values:xe(e("caretColor")),type:["color","any"]})},accentColor:({matchUtilities:r,theme:e})=>{r({accent:t=>({"accent-color":X(t)})},{values:xe(e("accentColor")),type:["color","any"]})},opacity:L("opacity",[["opacity",["opacity"]]]),backgroundBlendMode:({addUtilities:r})=>{r({".bg-blend-normal":{"background-blend-mode":"normal"},".bg-blend-multiply":{"background-blend-mode":"multiply"},".bg-blend-screen":{"background-blend-mode":"screen"},".bg-blend-overlay":{"background-blend-mode":"overlay"},".bg-blend-darken":{"background-blend-mode":"darken"},".bg-blend-lighten":{"background-blend-mode":"lighten"},".bg-blend-color-dodge":{"background-blend-mode":"color-dodge"},".bg-blend-color-burn":{"background-blend-mode":"color-burn"},".bg-blend-hard-light":{"background-blend-mode":"hard-light"},".bg-blend-soft-light":{"background-blend-mode":"soft-light"},".bg-blend-difference":{"background-blend-mode":"difference"},".bg-blend-exclusion":{"background-blend-mode":"exclusion"},".bg-blend-hue":{"background-blend-mode":"hue"},".bg-blend-saturation":{"background-blend-mode":"saturation"},".bg-blend-color":{"background-blend-mode":"color"},".bg-blend-luminosity":{"background-blend-mode":"luminosity"}})},mixBlendMode:({addUtilities:r})=>{r({".mix-blend-normal":{"mix-blend-mode":"normal"},".mix-blend-multiply":{"mix-blend-mode":"multiply"},".mix-blend-screen":{"mix-blend-mode":"screen"},".mix-blend-overlay":{"mix-blend-mode":"overlay"},".mix-blend-darken":{"mix-blend-mode":"darken"},".mix-blend-lighten":{"mix-blend-mode":"lighten"},".mix-blend-color-dodge":{"mix-blend-mode":"color-dodge"},".mix-blend-color-burn":{"mix-blend-mode":"color-burn"},".mix-blend-hard-light":{"mix-blend-mode":"hard-light"},".mix-blend-soft-light":{"mix-blend-mode":"soft-light"},".mix-blend-difference":{"mix-blend-mode":"difference"},".mix-blend-exclusion":{"mix-blend-mode":"exclusion"},".mix-blend-hue":{"mix-blend-mode":"hue"},".mix-blend-saturation":{"mix-blend-mode":"saturation"},".mix-blend-color":{"mix-blend-mode":"color"},".mix-blend-luminosity":{"mix-blend-mode":"luminosity"},".mix-blend-plus-darker":{"mix-blend-mode":"plus-darker"},".mix-blend-plus-lighter":{"mix-blend-mode":"plus-lighter"}})},boxShadow:(()=>{let r=mt("boxShadow"),e=["var(--tw-ring-offset-shadow, 0 0 #0000)","var(--tw-ring-shadow, 0 0 #0000)","var(--tw-shadow)"].join(", ");return function({matchUtilities:t,addDefaults:i,theme:n}){i("box-shadow",{"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),t({shadow:s=>{s=r(s);let a=en(s);for(let o of a)!o.valid||(o.color="var(--tw-shadow-color)");return{"@defaults box-shadow":{},"--tw-shadow":s==="none"?"0 0 #0000":s,"--tw-shadow-colored":s==="none"?"0 0 #0000":Lf(a),"box-shadow":e}}},{values:n("boxShadow"),type:["shadow"]})}})(),boxShadowColor:({matchUtilities:r,theme:e})=>{r({shadow:t=>({"--tw-shadow-color":X(t),"--tw-shadow":"var(--tw-shadow-colored)"})},{values:xe(e("boxShadowColor")),type:["color","any"]})},outlineStyle:({addUtilities:r})=>{r({".outline-none":{outline:"2px solid transparent","outline-offset":"2px"},".outline":{"outline-style":"solid"},".outline-dashed":{"outline-style":"dashed"},".outline-dotted":{"outline-style":"dotted"},".outline-double":{"outline-style":"double"}})},outlineWidth:L("outlineWidth",[["outline",["outline-width"]]],{type:["length","number","percentage"]}),outlineOffset:L("outlineOffset",[["outline-offset",["outline-offset"]]],{type:["length","number","percentage","any"],supportsNegativeValues:!0}),outlineColor:({matchUtilities:r,theme:e})=>{r({outline:t=>({"outline-color":X(t)})},{values:xe(e("outlineColor")),type:["color","any"]})},ringWidth:({matchUtilities:r,addDefaults:e,addUtilities:t,theme:i,config:n})=>{let s=(()=>{if(we(n(),"respectDefaultRingColorOpacity"))return i("ringColor.DEFAULT");let a=i("ringOpacity.DEFAULT","0.5");return i("ringColor")?.DEFAULT?Je(i("ringColor")?.DEFAULT,a,`rgb(147 197 253 / ${a})`):`rgb(147 197 253 / ${a})`})();e("ring-width",{"--tw-ring-inset":" ","--tw-ring-offset-width":i("ringOffsetWidth.DEFAULT","0px"),"--tw-ring-offset-color":i("ringOffsetColor.DEFAULT","#fff"),"--tw-ring-color":s,"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),r({ring:a=>({"@defaults ring-width":{},"--tw-ring-offset-shadow":"var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)","--tw-ring-shadow":`var(--tw-ring-inset) 0 0 0 calc(${a} + var(--tw-ring-offset-width)) var(--tw-ring-color)`,"box-shadow":["var(--tw-ring-offset-shadow)","var(--tw-ring-shadow)","var(--tw-shadow, 0 0 #0000)"].join(", ")})},{values:i("ringWidth"),type:"length"}),t({".ring-inset":{"@defaults ring-width":{},"--tw-ring-inset":"inset"}})},ringColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({ring:i=>t("ringOpacity")?Ae({color:i,property:"--tw-ring-color",variable:"--tw-ring-opacity"}):{"--tw-ring-color":X(i)}},{values:Object.fromEntries(Object.entries(xe(e("ringColor"))).filter(([i])=>i!=="DEFAULT")),type:["color","any"]})},ringOpacity:r=>{let{config:e}=r;return L("ringOpacity",[["ring-opacity",["--tw-ring-opacity"]]],{filterDefault:!we(e(),"respectDefaultRingColorOpacity")})(r)},ringOffsetWidth:L("ringOffsetWidth",[["ring-offset",["--tw-ring-offset-width"]]],{type:"length"}),ringOffsetColor:({matchUtilities:r,theme:e})=>{r({"ring-offset":t=>({"--tw-ring-offset-color":X(t)})},{values:xe(e("ringOffsetColor")),type:["color","any"]})},blur:({matchUtilities:r,theme:e})=>{r({blur:t=>({"--tw-blur":t.trim()===""?" ":`blur(${t})`,"@defaults filter":{},filter:nt})},{values:e("blur")})},brightness:({matchUtilities:r,theme:e})=>{r({brightness:t=>({"--tw-brightness":`brightness(${t})`,"@defaults filter":{},filter:nt})},{values:e("brightness")})},contrast:({matchUtilities:r,theme:e})=>{r({contrast:t=>({"--tw-contrast":`contrast(${t})`,"@defaults filter":{},filter:nt})},{values:e("contrast")})},dropShadow:({matchUtilities:r,theme:e})=>{r({"drop-shadow":t=>({"--tw-drop-shadow":Array.isArray(t)?t.map(i=>`drop-shadow(${i})`).join(" "):`drop-shadow(${t})`,"@defaults filter":{},filter:nt})},{values:e("dropShadow")})},grayscale:({matchUtilities:r,theme:e})=>{r({grayscale:t=>({"--tw-grayscale":`grayscale(${t})`,"@defaults filter":{},filter:nt})},{values:e("grayscale")})},hueRotate:({matchUtilities:r,theme:e})=>{r({"hue-rotate":t=>({"--tw-hue-rotate":`hue-rotate(${t})`,"@defaults filter":{},filter:nt})},{values:e("hueRotate"),supportsNegativeValues:!0})},invert:({matchUtilities:r,theme:e})=>{r({invert:t=>({"--tw-invert":`invert(${t})`,"@defaults filter":{},filter:nt})},{values:e("invert")})},saturate:({matchUtilities:r,theme:e})=>{r({saturate:t=>({"--tw-saturate":`saturate(${t})`,"@defaults filter":{},filter:nt})},{values:e("saturate")})},sepia:({matchUtilities:r,theme:e})=>{r({sepia:t=>({"--tw-sepia":`sepia(${t})`,"@defaults filter":{},filter:nt})},{values:e("sepia")})},filter:({addDefaults:r,addUtilities:e})=>{r("filter",{"--tw-blur":" ","--tw-brightness":" ","--tw-contrast":" ","--tw-grayscale":" ","--tw-hue-rotate":" ","--tw-invert":" ","--tw-saturate":" ","--tw-sepia":" ","--tw-drop-shadow":" "}),e({".filter":{"@defaults filter":{},filter:nt},".filter-none":{filter:"none"}})},backdropBlur:({matchUtilities:r,theme:e})=>{r({"backdrop-blur":t=>({"--tw-backdrop-blur":t.trim()===""?" ":`blur(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropBlur")})},backdropBrightness:({matchUtilities:r,theme:e})=>{r({"backdrop-brightness":t=>({"--tw-backdrop-brightness":`brightness(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropBrightness")})},backdropContrast:({matchUtilities:r,theme:e})=>{r({"backdrop-contrast":t=>({"--tw-backdrop-contrast":`contrast(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropContrast")})},backdropGrayscale:({matchUtilities:r,theme:e})=>{r({"backdrop-grayscale":t=>({"--tw-backdrop-grayscale":`grayscale(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropGrayscale")})},backdropHueRotate:({matchUtilities:r,theme:e})=>{r({"backdrop-hue-rotate":t=>({"--tw-backdrop-hue-rotate":`hue-rotate(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropHueRotate"),supportsNegativeValues:!0})},backdropInvert:({matchUtilities:r,theme:e})=>{r({"backdrop-invert":t=>({"--tw-backdrop-invert":`invert(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropInvert")})},backdropOpacity:({matchUtilities:r,theme:e})=>{r({"backdrop-opacity":t=>({"--tw-backdrop-opacity":`opacity(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropOpacity")})},backdropSaturate:({matchUtilities:r,theme:e})=>{r({"backdrop-saturate":t=>({"--tw-backdrop-saturate":`saturate(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropSaturate")})},backdropSepia:({matchUtilities:r,theme:e})=>{r({"backdrop-sepia":t=>({"--tw-backdrop-sepia":`sepia(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropSepia")})},backdropFilter:({addDefaults:r,addUtilities:e})=>{r("backdrop-filter",{"--tw-backdrop-blur":" ","--tw-backdrop-brightness":" ","--tw-backdrop-contrast":" ","--tw-backdrop-grayscale":" ","--tw-backdrop-hue-rotate":" ","--tw-backdrop-invert":" ","--tw-backdrop-opacity":" ","--tw-backdrop-saturate":" ","--tw-backdrop-sepia":" "}),e({".backdrop-filter":{"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge},".backdrop-filter-none":{"-webkit-backdrop-filter":"none","backdrop-filter":"none"}})},transitionProperty:({matchUtilities:r,theme:e})=>{let t=e("transitionTimingFunction.DEFAULT"),i=e("transitionDuration.DEFAULT");r({transition:n=>({"transition-property":n,...n==="none"?{}:{"transition-timing-function":t,"transition-duration":i}})},{values:e("transitionProperty")})},transitionDelay:L("transitionDelay",[["delay",["transitionDelay"]]]),transitionDuration:L("transitionDuration",[["duration",["transitionDuration"]]],{filterDefault:!0}),transitionTimingFunction:L("transitionTimingFunction",[["ease",["transitionTimingFunction"]]],{filterDefault:!0}),willChange:L("willChange",[["will-change",["will-change"]]]),contain:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-contain-size) var(--tw-contain-layout) var(--tw-contain-paint) var(--tw-contain-style)";r("contain",{"--tw-contain-size":" ","--tw-contain-layout":" ","--tw-contain-paint":" ","--tw-contain-style":" "}),e({".contain-none":{contain:"none"},".contain-content":{contain:"content"},".contain-strict":{contain:"strict"},".contain-size":{"@defaults contain":{},"--tw-contain-size":"size",contain:t},".contain-inline-size":{"@defaults contain":{},"--tw-contain-size":"inline-size",contain:t},".contain-layout":{"@defaults contain":{},"--tw-contain-layout":"layout",contain:t},".contain-paint":{"@defaults contain":{},"--tw-contain-paint":"paint",contain:t},".contain-style":{"@defaults contain":{},"--tw-contain-style":"style",contain:t}})},content:L("content",[["content",["--tw-content",["content","var(--tw-content)"]]]]),forcedColorAdjust:({addUtilities:r})=>{r({".forced-color-adjust-auto":{"forced-color-adjust":"auto"},".forced-color-adjust-none":{"forced-color-adjust":"none"}})}}});function h_(r){if(r===void 0)return!1;if(r==="true"||r==="1")return!0;if(r==="false"||r==="0")return!1;if(r==="*")return!0;let e=r.split(",").map(t=>t.split(":")[0]);return e.includes("-tailwindcss")?!1:!!e.includes("tailwindcss")}var Ze,wh,vh,es,No,gt,Ti,It=P(()=>{u();Ze=typeof m!="undefined"?{NODE_ENV:"production",DEBUG:h_(m.env.DEBUG)}:{NODE_ENV:"production",DEBUG:!1},wh=new Map,vh=new Map,es=new Map,No=new Map,gt=new String("*"),Ti=Symbol("__NONE__")});function cr(r){let e=[],t=!1;for(let i=0;i0)}var xh,kh,m_,Bo=P(()=>{u();xh=new Map([["{","}"],["[","]"],["(",")"]]),kh=new Map(Array.from(xh.entries()).map(([r,e])=>[e,r])),m_=new Set(['"',"'","`"])});function pr(r){let[e]=Sh(r);return e.forEach(([t,i])=>t.removeChild(i)),r.nodes.push(...e.map(([,t])=>t)),r}function Sh(r){let e=[],t=null;for(let i of r.nodes)if(i.type==="combinator")e=e.filter(([,n])=>jo(n).includes("jumpable")),t=null;else if(i.type==="pseudo"){g_(i)?(t=i,e.push([r,i,null])):t&&y_(i,t)?e.push([r,i,t]):t=null;for(let n of i.nodes??[]){let[s,a]=Sh(n);t=a||t,e.push(...s)}}return[e,t]}function Ah(r){return r.value.startsWith("::")||Fo[r.value]!==void 0}function g_(r){return Ah(r)&&jo(r).includes("terminal")}function y_(r,e){return r.type!=="pseudo"||Ah(r)?!1:jo(e).includes("actionable")}function jo(r){return Fo[r.value]??Fo.__default__}var Fo,ts=P(()=>{u();Fo={"::after":["terminal","jumpable"],"::backdrop":["terminal","jumpable"],"::before":["terminal","jumpable"],"::cue":["terminal"],"::cue-region":["terminal"],"::first-letter":["terminal","jumpable"],"::first-line":["terminal","jumpable"],"::grammar-error":["terminal"],"::marker":["terminal","jumpable"],"::part":["terminal","actionable"],"::placeholder":["terminal","jumpable"],"::selection":["terminal","jumpable"],"::slotted":["terminal"],"::spelling-error":["terminal"],"::target-text":["terminal"],"::file-selector-button":["terminal","actionable"],"::deep":["actionable"],"::v-deep":["actionable"],"::ng-deep":["actionable"],":after":["terminal","jumpable"],":before":["terminal","jumpable"],":first-letter":["terminal","jumpable"],":first-line":["terminal","jumpable"],":where":[],":is":[],":has":[],__default__:["terminal","actionable"]}});function dr(r,{context:e,candidate:t}){let i=e?.tailwindConfig.prefix??"",n=r.map(a=>{let o=(0,st.default)().astSync(a.format);return{...a,ast:a.respectPrefix?ur(i,o):o}}),s=st.default.root({nodes:[st.default.selector({nodes:[st.default.className({value:Te(t)})]})]});for(let{ast:a}of n)[s,a]=w_(s,a),a.walkNesting(o=>o.replaceWith(...s.nodes[0].nodes)),s=a;return s}function _h(r){let e=[];for(;r.prev()&&r.prev().type!=="combinator";)r=r.prev();for(;r&&r.type!=="combinator";)e.push(r),r=r.next();return e}function b_(r){return r.sort((e,t)=>e.type==="tag"&&t.type==="class"?-1:e.type==="class"&&t.type==="tag"?1:e.type==="class"&&t.type==="pseudo"&&t.value.startsWith("::")?-1:e.type==="pseudo"&&e.value.startsWith("::")&&t.type==="class"?1:r.index(e)-r.index(t)),r}function Uo(r,e){let t=!1;r.walk(i=>{if(i.type==="class"&&i.value===e)return t=!0,!1}),t||r.remove()}function rs(r,e,{context:t,candidate:i,base:n}){let s=t?.tailwindConfig?.separator??":";n=n??ve(i,s).pop();let a=(0,st.default)().astSync(r);if(a.walkClasses(f=>{f.raws&&f.value.includes(n)&&(f.raws.value=Te((0,Ch.default)(f.raws.value)))}),a.each(f=>Uo(f,n)),a.length===0)return null;let o=Array.isArray(e)?dr(e,{context:t,candidate:i}):e;if(o===null)return a.toString();let l=st.default.comment({value:"/*__simple__*/"}),c=st.default.comment({value:"/*__simple__*/"});return a.walkClasses(f=>{if(f.value!==n)return;let d=f.parent,p=o.nodes[0].nodes;if(d.nodes.length===1){f.replaceWith(...p);return}let h=_h(f);d.insertBefore(h[0],l),d.insertAfter(h[h.length-1],c);for(let v of p)d.insertBefore(h[0],v.clone());f.remove(),h=_h(l);let b=d.index(l);d.nodes.splice(b,h.length,...b_(st.default.selector({nodes:h})).nodes),l.remove(),c.remove()}),a.walkPseudos(f=>{f.value===zo&&f.replaceWith(f.nodes)}),a.each(f=>pr(f)),a.toString()}function w_(r,e){let t=[];return r.walkPseudos(i=>{i.value===zo&&t.push({pseudo:i,value:i.nodes[0].toString()})}),e.walkPseudos(i=>{if(i.value!==zo)return;let n=i.nodes[0].toString(),s=t.find(c=>c.value===n);if(!s)return;let a=[],o=i.next();for(;o&&o.type!=="combinator";)a.push(o),o=o.next();let l=o;s.pseudo.parent.insertAfter(s.pseudo,st.default.selector({nodes:a.map(c=>c.clone())})),i.remove(),a.forEach(c=>c.remove()),l&&l.type==="combinator"&&l.remove()}),[r,e]}var st,Ch,zo,Vo=P(()=>{u();st=pe(it()),Ch=pe(Pn());fr();Gn();ts();zt();zo=":merge"});function is(r,e){let t=(0,Ho.default)().astSync(r);return t.each(i=>{i.nodes.some(s=>s.type==="combinator")&&(i.nodes=[Ho.default.pseudo({value:":is",nodes:[i.clone()]})]),pr(i)}),`${e} ${t.toString()}`}var Ho,Wo=P(()=>{u();Ho=pe(it());ts()});function Go(r){return v_.transformSync(r)}function*x_(r){let e=1/0;for(;e>=0;){let t,i=!1;if(e===1/0&&r.endsWith("]")){let a=r.indexOf("[");r[a-1]==="-"?t=a-1:r[a-1]==="/"?(t=a-1,i=!0):t=-1}else e===1/0&&r.includes("/")?(t=r.lastIndexOf("/"),i=!0):t=r.lastIndexOf("-",e);if(t<0)break;let n=r.slice(0,t),s=r.slice(i?t:t+1);e=t-1,!(n===""||s==="/")&&(yield[n,s])}}function k_(r,e){if(r.length===0||e.tailwindConfig.prefix==="")return r;for(let t of r){let[i]=t;if(i.options.respectPrefix){let n=ee.root({nodes:[t[1].clone()]}),s=t[1].raws.tailwind.classCandidate;n.walkRules(a=>{let o=s.startsWith("-");a.selector=ur(e.tailwindConfig.prefix,a.selector,o)}),t[1]=n.nodes[0]}}return r}function S_(r,e){if(r.length===0)return r;let t=[];function i(n){return n.parent&&n.parent.type==="atrule"&&n.parent.name==="keyframes"}for(let[n,s]of r){let a=ee.root({nodes:[s.clone()]});a.walkRules(o=>{if(i(o))return;let l=(0,ns.default)().astSync(o.selector);l.each(c=>Uo(c,e)),Qf(l,c=>c===e?`!${c}`:c),o.selector=l.toString(),o.walkDecls(c=>c.important=!0)}),t.push([{...n,important:!0},a.nodes[0]])}return t}function A_(r,e,t){if(e.length===0)return e;let i={modifier:null,value:Ti};{let[n,...s]=ve(r,"/");if(s.length>1&&(n=n+"/"+s.slice(0,-1).join("/"),s=s.slice(-1)),s.length&&!t.variantMap.has(r)&&(r=n,i.modifier=s[0],!we(t.tailwindConfig,"generalizedModifiers")))return[]}if(r.endsWith("]")&&!r.startsWith("[")){let n=/(.)(-?)\[(.*)\]/g.exec(r);if(n){let[,s,a,o]=n;if(s==="@"&&a==="-")return[];if(s!=="@"&&a==="")return[];r=r.replace(`${a}[${o}]`,""),i.value=o}}if(Ko(r)&&!t.variantMap.has(r)){let n=t.offsets.recordVariant(r),s=K(r.slice(1,-1)),a=ve(s,",");if(a.length>1)return[];if(!a.every(ls))return[];let o=a.map((l,c)=>[t.offsets.applyParallelOffset(n,c),Ri(l.trim())]);t.variantMap.set(r,o)}if(t.variantMap.has(r)){let n=Ko(r),s=t.variantOptions.get(r)?.[Pt]??{},a=t.variantMap.get(r).slice(),o=[],l=(()=>!(n||s.respectPrefix===!1))();for(let[c,f]of e){if(c.layer==="user")continue;let d=ee.root({nodes:[f.clone()]});for(let[p,h,b]of a){let w=function(){v.raws.neededBackup||(v.raws.neededBackup=!0,v.walkRules(T=>T.raws.originalSelector=T.selector))},k=function(T){return w(),v.each(B=>{B.type==="rule"&&(B.selectors=B.selectors.map(N=>T({get className(){return Go(N)},selector:N})))}),v},v=(b??d).clone(),y=[],S=h({get container(){return w(),v},separator:t.tailwindConfig.separator,modifySelectors:k,wrap(T){let B=v.nodes;v.removeAll(),T.append(B),v.append(T)},format(T){y.push({format:T,respectPrefix:l})},args:i});if(Array.isArray(S)){for(let[T,B]of S.entries())a.push([t.offsets.applyParallelOffset(p,T),B,v.clone()]);continue}if(typeof S=="string"&&y.push({format:S,respectPrefix:l}),S===null)continue;v.raws.neededBackup&&(delete v.raws.neededBackup,v.walkRules(T=>{let B=T.raws.originalSelector;if(!B||(delete T.raws.originalSelector,B===T.selector))return;let N=T.selector,R=(0,ns.default)(F=>{F.walkClasses(Y=>{Y.value=`${r}${t.tailwindConfig.separator}${Y.value}`})}).processSync(B);y.push({format:N.replace(R,"&"),respectPrefix:l}),T.selector=B})),v.nodes[0].raws.tailwind={...v.nodes[0].raws.tailwind,parentLayer:c.layer};let E=[{...c,sort:t.offsets.applyVariantOffset(c.sort,p,Object.assign(i,t.variantOptions.get(r))),collectedFormats:(c.collectedFormats??[]).concat(y)},v.nodes[0]];o.push(E)}}return o}return[]}function Qo(r,e,t={}){return!ke(r)&&!Array.isArray(r)?[[r],t]:Array.isArray(r)?Qo(r[0],e,r[1]):(e.has(r)||e.set(r,lr(r)),[e.get(r),t])}function __(r){return C_.test(r)}function E_(r){if(!r.includes("://"))return!1;try{let e=new URL(r);return e.scheme!==""&&e.host!==""}catch(e){return!1}}function Eh(r){let e=!0;return r.walkDecls(t=>{if(!Oh(t.prop,t.value))return e=!1,!1}),e}function Oh(r,e){if(E_(`${r}:${e}`))return!1;try{return ee.parse(`a{${r}:${e}}`).toResult(),!0}catch(t){return!1}}function O_(r,e){let[,t,i]=r.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/)??[];if(i===void 0||!__(t)||!cr(i))return null;let n=K(i,{property:t});return Oh(t,n)?[[{sort:e.offsets.arbitraryProperty(r),layer:"utilities",options:{respectImportant:!0}},()=>({[$o(r)]:{[t]:n}})]]:null}function*T_(r,e){e.candidateRuleMap.has(r)&&(yield[e.candidateRuleMap.get(r),"DEFAULT"]),yield*function*(o){o!==null&&(yield[o,"DEFAULT"])}(O_(r,e));let t=r,i=!1,n=e.tailwindConfig.prefix,s=n.length,a=t.startsWith(n)||t.startsWith(`-${n}`);t[s]==="-"&&a&&(i=!0,t=n+t.slice(s+1)),i&&e.candidateRuleMap.has(t)&&(yield[e.candidateRuleMap.get(t),"-DEFAULT"]);for(let[o,l]of x_(t))e.candidateRuleMap.has(o)&&(yield[e.candidateRuleMap.get(o),i?`-${l}`:l])}function R_(r,e){return r===gt?[gt]:ve(r,e)}function*P_(r,e){for(let t of r)t[1].raws.tailwind={...t[1].raws.tailwind,classCandidate:e,preserveSource:t[0].options?.preserveSource??!1},yield t}function*Yo(r,e){let t=e.tailwindConfig.separator,[i,...n]=R_(r,t).reverse(),s=!1;i.startsWith("!")&&(s=!0,i=i.slice(1));for(let a of T_(i,e)){let o=[],l=new Map,[c,f]=a,d=c.length===1;for(let[p,h]of c){let b=[];if(typeof h=="function")for(let v of[].concat(h(f,{isOnlyPlugin:d}))){let[y,w]=Qo(v,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}else if(f==="DEFAULT"||f==="-DEFAULT"){let v=h,[y,w]=Qo(v,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}if(b.length>0){let v=Array.from(ta(p.options?.types??[],f,p.options??{},e.tailwindConfig)).map(([y,w])=>w);v.length>0&&l.set(b,v),o.push(b)}}if(Ko(f)){if(o.length>1){let b=function(y){return y.length===1?y[0]:y.find(w=>{let k=l.get(w);return w.some(([{options:S},E])=>Eh(E)?S.types.some(({type:T,preferOnConflict:B})=>k.includes(T)&&B):!1)})},[p,h]=o.reduce((y,w)=>(w.some(([{options:S}])=>S.types.some(({type:E})=>E==="any"))?y[0].push(w):y[1].push(w),y),[[],[]]),v=b(h)??b(p);if(v)o=[v];else{let y=o.map(k=>new Set([...l.get(k)??[]]));for(let k of y)for(let S of k){let E=!1;for(let T of y)k!==T&&T.has(S)&&(T.delete(S),E=!0);E&&k.delete(S)}let w=[];for(let[k,S]of y.entries())for(let E of S){let T=o[k].map(([,B])=>B).flat().map(B=>B.toString().split(` +`).slice(1,-1).map(N=>N.trim()).map(N=>` ${N}`).join(` +`)).join(` + +`);w.push(` Use \`${r.replace("[",`[${E}:`)}\` for \`${T.trim()}\``);break}G.warn([`The class \`${r}\` is ambiguous and matches multiple utilities.`,...w,`If this is content and not a class, replace it with \`${r.replace("[","[").replace("]","]")}\` to silence this warning.`]);continue}}o=o.map(p=>p.filter(h=>Eh(h[1])))}o=o.flat(),o=Array.from(P_(o,i)),o=k_(o,e),s&&(o=S_(o,i));for(let p of n)o=A_(p,o,e);for(let p of o)p[1].raws.tailwind={...p[1].raws.tailwind,candidate:r},p=I_(p,{context:e,candidate:r}),p!==null&&(yield p)}}function I_(r,{context:e,candidate:t}){if(!r[0].collectedFormats)return r;let i=!0,n;try{n=dr(r[0].collectedFormats,{context:e,candidate:t})}catch{return null}let s=ee.root({nodes:[r[1].clone()]});return s.walkRules(a=>{if(!ss(a))try{let o=rs(a.selector,n,{candidate:t,context:e});if(o===null){a.remove();return}a.selector=o}catch{return i=!1,!1}}),!i||s.nodes.length===0?null:(r[1]=s.nodes[0],r)}function ss(r){return r.parent&&r.parent.type==="atrule"&&r.parent.name==="keyframes"}function D_(r){if(r===!0)return e=>{ss(e)||e.walkDecls(t=>{t.parent.type==="rule"&&!ss(t.parent)&&(t.important=!0)})};if(typeof r=="string")return e=>{ss(e)||(e.selectors=e.selectors.map(t=>is(t,r)))}}function as(r,e,t=!1){let i=[],n=D_(e.tailwindConfig.important);for(let s of r){if(e.notClassCache.has(s))continue;if(e.candidateRuleCache.has(s)){i=i.concat(Array.from(e.candidateRuleCache.get(s)));continue}let a=Array.from(Yo(s,e));if(a.length===0){e.notClassCache.add(s);continue}e.classCache.set(s,a);let o=e.candidateRuleCache.get(s)??new Set;e.candidateRuleCache.set(s,o);for(let l of a){let[{sort:c,options:f},d]=l;if(f.respectImportant&&n){let h=ee.root({nodes:[d.clone()]});h.walkRules(n),d=h.nodes[0]}let p=[c,t?d.clone():d];o.add(p),e.ruleCache.add(p),i.push(p)}}return i}function Ko(r){return r.startsWith("[")&&r.endsWith("]")}var ns,v_,C_,os=P(()=>{u();Ot();ns=pe(it());qo();Kt();Gn();Fr();Be();It();Vo();Lo();Br();Oi();Bo();zt();ct();Wo();v_=(0,ns.default)(r=>r.first.filter(({type:e})=>e==="class").pop().value);C_=/^[a-z_-]/});var Th,Rh=P(()=>{u();Th={}});function q_(r){try{return Th.createHash("md5").update(r,"utf-8").digest("binary")}catch(e){return""}}function Ph(r,e){let t=e.toString();if(!t.includes("@tailwind"))return!1;let i=No.get(r),n=q_(t),s=i!==n;return No.set(r,n),s}var Ih=P(()=>{u();Rh();It()});function us(r){return(r>0n)-(r<0n)}var Dh=P(()=>{u()});function qh(r,e){let t=0n,i=0n;for(let[n,s]of e)r&n&&(t=t|n,i=i|s);return r&~t|i}var $h=P(()=>{u()});function Lh(r){let e=null;for(let t of r)e=e??t,e=e>t?e:t;return e}function $_(r,e){let t=r.length,i=e.length,n=t{u();Dh();$h();Xo=class{constructor(){this.offsets={defaults:0n,base:0n,components:0n,utilities:0n,variants:0n,user:0n},this.layerPositions={defaults:0n,base:1n,components:2n,utilities:3n,user:4n,variants:5n},this.reservedVariantBits=0n,this.variantOffsets=new Map}create(e){return{layer:e,parentLayer:e,arbitrary:0n,variants:0n,parallelIndex:0n,index:this.offsets[e]++,propertyOffset:0n,property:"",options:[]}}arbitraryProperty(e){return{...this.create("utilities"),arbitrary:1n,property:e}}forVariant(e,t=0){let i=this.variantOffsets.get(e);if(i===void 0)throw new Error(`Cannot find offset for unknown variant ${e}`);return{...this.create("variants"),variants:i<n.startsWith("[")).sort(([n],[s])=>$_(n,s)),t=e.map(([,n])=>n).sort((n,s)=>us(n-s));return e.map(([,n],s)=>[n,t[s]]).filter(([n,s])=>n!==s)}remapArbitraryVariantOffsets(e){let t=this.recalculateVariantOffsets();return t.length===0?e:e.map(i=>{let[n,s]=i;return n={...n,variants:qh(n.variants,t)},[n,s]})}sortArbitraryProperties(e){let t=new Set;for(let[a]of e)a.arbitrary===1n&&t.add(a.property);if(t.size===0)return e;let i=Array.from(t).sort(),n=new Map,s=1n;for(let a of i)n.set(a,s++);return e.map(a=>{let[o,l]=a;return o={...o,propertyOffset:n.get(o.property)??0n},[o,l]})}sort(e){return e=this.remapArbitraryVariantOffsets(e),e=this.sortArbitraryProperties(e),e.sort(([t],[i])=>us(this.compare(t,i)))}}});function tl(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function Bh({type:r="any",...e}){let t=[].concat(r);return{...e,types:t.map(i=>Array.isArray(i)?{type:i[0],...i[1]}:{type:i,preferOnConflict:!1})}}function L_(r){let e=[],t="",i=0;for(let n=0;n0&&e.push(t.trim()),e=e.filter(n=>n!==""),e}function M_(r,e,{before:t=[]}={}){if(t=[].concat(t),t.length<=0){r.push(e);return}let i=r.length-1;for(let n of t){let s=r.indexOf(n);s!==-1&&(i=Math.min(i,s))}r.splice(i,0,e)}function Fh(r){return Array.isArray(r)?r.flatMap(e=>!Array.isArray(e)&&!ke(e)?e:lr(e)):Fh([r])}function N_(r,e){return(0,Zo.default)(i=>{let n=[];return e&&e(i),i.walkClasses(s=>{n.push(s.value)}),n}).transformSync(r)}function B_(r){r.walkPseudos(e=>{e.value===":not"&&e.remove()})}function F_(r,e={containsNonOnDemandable:!1},t=0){let i=[],n=[];r.type==="rule"?n.push(...r.selectors):r.type==="atrule"&&r.walkRules(s=>n.push(...s.selectors));for(let s of n){let a=N_(s,B_);a.length===0&&(e.containsNonOnDemandable=!0);for(let o of a)i.push(o)}return t===0?[e.containsNonOnDemandable||i.length===0,i]:i}function fs(r){return Fh(r).flatMap(e=>{let t=new Map,[i,n]=F_(e);return i&&n.unshift(gt),n.map(s=>(t.has(e)||t.set(e,e),[s,t.get(e)]))})}function ls(r){return r.startsWith("@")||r.includes("&")}function Ri(r){r=r.replace(/\n+/g,"").replace(/\s{1,}/g," ").trim();let e=L_(r).map(t=>{if(!t.startsWith("@"))return({format:s})=>s(t);let[,i,n]=/@(\S*)( .+|[({].*)?/g.exec(t);return({wrap:s})=>s(ee.atRule({name:i,params:n?.trim()??""}))}).reverse();return t=>{for(let i of e)i(t)}}function j_(r,e,{variantList:t,variantMap:i,offsets:n,classList:s}){function a(p,h){return p?(0,Nh.default)(r,p,h):r}function o(p){return ur(r.prefix,p)}function l(p,h){return p===gt?gt:h.respectPrefix?e.tailwindConfig.prefix+p:p}function c(p,h,b={}){let v=kt(p),y=a(["theme",...v],h);return mt(v[0])(y,b)}let f=0,d={postcss:ee,prefix:o,e:Te,config:a,theme:c,corePlugins:p=>Array.isArray(r.corePlugins)?r.corePlugins.includes(p):a(["corePlugins",p],!0),variants:()=>[],addBase(p){for(let[h,b]of fs(p)){let v=l(h,{}),y=n.create("base");e.candidateRuleMap.has(v)||e.candidateRuleMap.set(v,[]),e.candidateRuleMap.get(v).push([{sort:y,layer:"base"},b])}},addDefaults(p,h){let b={[`@defaults ${p}`]:h};for(let[v,y]of fs(b)){let w=l(v,{});e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("defaults"),layer:"defaults"},y])}},addComponents(p,h){h=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!1},Array.isArray(h)?{}:h);for(let[v,y]of fs(p)){let w=l(v,h);s.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("components"),layer:"components",options:h},y])}},addUtilities(p,h){h=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!0},Array.isArray(h)?{}:h);for(let[v,y]of fs(p)){let w=l(v,h);s.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("utilities"),layer:"utilities",options:h},y])}},matchUtilities:function(p,h){h=Bh({...{respectPrefix:!0,respectImportant:!0,modifiers:!1},...h});let v=n.create("utilities");for(let y in p){let S=function(T,{isOnlyPlugin:B}){let[N,R,F]=ea(h.types,T,h,r);if(N===void 0)return[];if(!h.types.some(({type:U})=>U===R))if(B)G.warn([`Unnecessary typehint \`${R}\` in \`${y}-${T}\`.`,`You can safely update it to \`${y}-${T.replace(R+":","")}\`.`]);else return[];if(!cr(N))return[];let Y={get modifier(){return h.modifiers||G.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),F}},_=we(r,"generalizedModifiers");return[].concat(_?k(N,Y):k(N)).filter(Boolean).map(U=>({[Qn(y,T)]:U}))},w=l(y,h),k=p[y];s.add([w,h]);let E=[{sort:v,layer:"utilities",options:h},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(E)}},matchComponents:function(p,h){h=Bh({...{respectPrefix:!0,respectImportant:!1,modifiers:!1},...h});let v=n.create("components");for(let y in p){let S=function(T,{isOnlyPlugin:B}){let[N,R,F]=ea(h.types,T,h,r);if(N===void 0)return[];if(!h.types.some(({type:U})=>U===R))if(B)G.warn([`Unnecessary typehint \`${R}\` in \`${y}-${T}\`.`,`You can safely update it to \`${y}-${T.replace(R+":","")}\`.`]);else return[];if(!cr(N))return[];let Y={get modifier(){return h.modifiers||G.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),F}},_=we(r,"generalizedModifiers");return[].concat(_?k(N,Y):k(N)).filter(Boolean).map(U=>({[Qn(y,T)]:U}))},w=l(y,h),k=p[y];s.add([w,h]);let E=[{sort:v,layer:"components",options:h},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(E)}},addVariant(p,h,b={}){h=[].concat(h).map(v=>{if(typeof v!="string")return(y={})=>{let{args:w,modifySelectors:k,container:S,separator:E,wrap:T,format:B}=y,N=v(Object.assign({modifySelectors:k,container:S,separator:E},b.type===Jo.MatchVariant&&{args:w,wrap:T,format:B}));if(typeof N=="string"&&!ls(N))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Array.isArray(N)?N.filter(R=>typeof R=="string").map(R=>Ri(R)):N&&typeof N=="string"&&Ri(N)(y)};if(!ls(v))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Ri(v)}),M_(t,p,b),i.set(p,h),e.variantOptions.set(p,b)},matchVariant(p,h,b){let v=b?.id??++f,y=p==="@",w=we(r,"generalizedModifiers");for(let[S,E]of Object.entries(b?.values??{}))S!=="DEFAULT"&&d.addVariant(y?`${p}${S}`:`${p}-${S}`,({args:T,container:B})=>h(E,w?{modifier:T?.modifier,container:B}:{container:B}),{...b,value:E,id:v,type:Jo.MatchVariant,variantInfo:el.Base});let k="DEFAULT"in(b?.values??{});d.addVariant(p,({args:S,container:E})=>S?.value===Ti&&!k?null:h(S?.value===Ti?b.values.DEFAULT:S?.value??(typeof S=="string"?S:""),w?{modifier:S?.modifier,container:E}:{container:E}),{...b,id:v,type:Jo.MatchVariant,variantInfo:el.Dynamic})}};return d}function cs(r){return rl.has(r)||rl.set(r,new Map),rl.get(r)}function jh(r,e){let t=!1,i=new Map;for(let n of r){if(!n)continue;let s=oa.parse(n),a=s.hash?s.href.replace(s.hash,""):s.href;a=s.search?a.replace(s.search,""):a;let o=be.statSync(decodeURIComponent(a),{throwIfNoEntry:!1})?.mtimeMs;!o||((!e.has(n)||o>e.get(n))&&(t=!0),i.set(n,o))}return[t,i]}function zh(r){r.walkAtRules(e=>{["responsive","variants"].includes(e.name)&&(zh(e),e.before(e.nodes),e.remove())})}function z_(r){let e=[];return r.each(t=>{t.type==="atrule"&&["responsive","variants"].includes(t.name)&&(t.name="layer",t.params="utilities")}),r.walkAtRules("layer",t=>{if(zh(t),t.params==="base"){for(let i of t.nodes)e.push(function({addBase:n}){n(i,{respectPrefix:!1})});t.remove()}else if(t.params==="components"){for(let i of t.nodes)e.push(function({addComponents:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}else if(t.params==="utilities"){for(let i of t.nodes)e.push(function({addUtilities:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}}),e}function U_(r,e){let t=Object.entries({...se,...yh}).map(([l,c])=>r.tailwindConfig.corePlugins.includes(l)?c:null).filter(Boolean),i=r.tailwindConfig.plugins.map(l=>(l.__isOptionsFunction&&(l=l()),typeof l=="function"?l:l.handler)),n=z_(e),s=[se.childVariant,se.pseudoElementVariants,se.pseudoClassVariants,se.hasVariants,se.ariaVariants,se.dataVariants],a=[se.supportsVariants,se.reducedMotionVariants,se.prefersContrastVariants,se.screenVariants,se.orientationVariants,se.directionVariants,se.darkVariants,se.forcedColorsVariants,se.printVariant];return(r.tailwindConfig.darkMode==="class"||Array.isArray(r.tailwindConfig.darkMode)&&r.tailwindConfig.darkMode[0]==="class")&&(a=[se.supportsVariants,se.reducedMotionVariants,se.prefersContrastVariants,se.darkVariants,se.screenVariants,se.orientationVariants,se.directionVariants,se.forcedColorsVariants,se.printVariant]),[...t,...s,...i,...a,...n]}function V_(r,e){let t=[],i=new Map;e.variantMap=i;let n=new Xo;e.offsets=n;let s=new Set,a=j_(e.tailwindConfig,e,{variantList:t,variantMap:i,offsets:n,classList:s});for(let f of r)if(Array.isArray(f))for(let d of f)d(a);else f?.(a);n.recordVariants(t,f=>i.get(f).length);for(let[f,d]of i.entries())e.variantMap.set(f,d.map((p,h)=>[n.forVariant(f,h),p]));let o=(e.tailwindConfig.safelist??[]).filter(Boolean);if(o.length>0){let f=[];for(let d of o){if(typeof d=="string"){e.changedContent.push({content:d,extension:"html"});continue}if(d instanceof RegExp){G.warn("root-regex",["Regular expressions in `safelist` work differently in Tailwind CSS v3.0.","Update your `safelist` configuration to eliminate this warning.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"]);continue}f.push(d)}if(f.length>0){let d=new Map,p=e.tailwindConfig.prefix.length,h=f.some(b=>b.pattern.source.includes("!"));for(let b of s){let v=Array.isArray(b)?(()=>{let[y,w]=b,S=Object.keys(w?.values??{}).map(E=>Ei(y,E));return w?.supportsNegativeValues&&(S=[...S,...S.map(E=>"-"+E)],S=[...S,...S.map(E=>E.slice(0,p)+"-"+E.slice(p))]),w.types.some(({type:E})=>E==="color")&&(S=[...S,...S.flatMap(E=>Object.keys(e.tailwindConfig.theme.opacity).map(T=>`${E}/${T}`))]),h&&w?.respectImportant&&(S=[...S,...S.map(E=>"!"+E)]),S})():[b];for(let y of v)for(let{pattern:w,variants:k=[]}of f)if(w.lastIndex=0,d.has(w)||d.set(w,0),!!w.test(y)){d.set(w,d.get(w)+1),e.changedContent.push({content:y,extension:"html"});for(let S of k)e.changedContent.push({content:S+e.tailwindConfig.separator+y,extension:"html"})}}for(let[b,v]of d.entries())v===0&&G.warn([`The safelist pattern \`${b}\` doesn't match any Tailwind CSS classes.`,"Fix this pattern or remove it from your `safelist` configuration.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"])}}let l=[].concat(e.tailwindConfig.darkMode??"media")[1]??"dark",c=[tl(e,l),tl(e,"group"),tl(e,"peer")];e.getClassOrder=function(d){let p=[...d].sort((y,w)=>y===w?0:y[y,null])),b=as(new Set(p),e,!0);b=e.offsets.sort(b);let v=BigInt(c.length);for(let[,y]of b){let w=y.raws.tailwind.candidate;h.set(w,h.get(w)??v++)}return d.map(y=>{let w=h.get(y)??null,k=c.indexOf(y);return w===null&&k!==-1&&(w=BigInt(k)),[y,w]})},e.getClassList=function(d={}){let p=[];for(let h of s)if(Array.isArray(h)){let[b,v]=h,y=[],w=Object.keys(v?.modifiers??{});v?.types?.some(({type:E})=>E==="color")&&w.push(...Object.keys(e.tailwindConfig.theme.opacity??{}));let k={modifiers:w},S=d.includeMetadata&&w.length>0;for(let[E,T]of Object.entries(v?.values??{})){if(T==null)continue;let B=Ei(b,E);if(p.push(S?[B,k]:B),v?.supportsNegativeValues&&xt(T)){let N=Ei(b,`-${E}`);y.push(S?[N,k]:N)}}p.push(...y)}else p.push(h);return p},e.getVariants=function(){let d=Math.random().toString(36).substring(7).toUpperCase(),p=[];for(let[h,b]of e.variantOptions.entries())b.variantInfo!==el.Base&&p.push({name:h,isArbitrary:b.type===Symbol.for("MATCH_VARIANT"),values:Object.keys(b.values??{}),hasDash:h!=="@",selectors({modifier:v,value:y}={}){let w=`TAILWINDPLACEHOLDER${d}`,k=ee.rule({selector:`.${w}`}),S=ee.root({nodes:[k.clone()]}),E=S.toString(),T=(e.variantMap.get(h)??[]).flatMap(([le,A])=>A),B=[];for(let le of T){let A=[],C={args:{modifier:v,value:b.values?.[y]??y},separator:e.tailwindConfig.separator,modifySelectors(V){return S.each(Ee=>{Ee.type==="rule"&&(Ee.selectors=Ee.selectors.map(Ie=>V({get className(){return Go(Ie)},selector:Ie})))}),S},format(V){A.push(V)},wrap(V){A.push(`@${V.name} ${V.params} { & }`)},container:S},he=le(C);if(A.length>0&&B.push(A),Array.isArray(he))for(let V of he)A=[],V(C),B.push(A)}let N=[],R=S.toString();E!==R&&(S.walkRules(le=>{let A=le.selector,C=(0,Zo.default)(he=>{he.walkClasses(V=>{V.value=`${h}${e.tailwindConfig.separator}${V.value}`})}).processSync(A);N.push(A.replace(C,"&").replace(w,"&"))}),S.walkAtRules(le=>{N.push(`@${le.name} (${le.params}) { & }`)}));let F=!(y in(b.values??{})),Y=b[Pt]??{},_=(()=>!(F||Y.respectPrefix===!1))();B=B.map(le=>le.map(A=>({format:A,respectPrefix:_}))),N=N.map(le=>({format:le,respectPrefix:_}));let Q={candidate:w,context:e},U=B.map(le=>rs(`.${w}`,dr(le,Q),Q).replace(`.${w}`,"&").replace("{ & }","").trim());return N.length>0&&U.push(dr(N,Q).toString().replace(`.${w}`,"&")),U}});return p}}function Uh(r,e){!r.classCache.has(e)||(r.notClassCache.add(e),r.classCache.delete(e),r.applyClassCache.delete(e),r.candidateRuleMap.delete(e),r.candidateRuleCache.delete(e),r.stylesheetCache=null)}function H_(r,e){let t=e.raws.tailwind.candidate;if(!!t){for(let i of r.ruleCache)i[1].raws.tailwind.candidate===t&&r.ruleCache.delete(i);Uh(r,t)}}function il(r,e=[],t=ee.root()){let i={disposables:[],ruleCache:new Set,candidateRuleCache:new Map,classCache:new Map,applyClassCache:new Map,notClassCache:new Set(r.blocklist??[]),postCssNodeCache:new Map,candidateRuleMap:new Map,tailwindConfig:r,changedContent:e,variantMap:new Map,stylesheetCache:null,variantOptions:new Map,markInvalidUtilityCandidate:s=>Uh(i,s),markInvalidUtilityNode:s=>H_(i,s)},n=U_(i,t);return V_(n,i),i}function Vh(r,e,t,i,n,s){let a=e.opts.from,o=i!==null;Ze.DEBUG&&console.log("Source path:",a);let l;if(o&&hr.has(a))l=hr.get(a);else if(Pi.has(n)){let p=Pi.get(n);Dt.get(p).add(a),hr.set(a,p),l=p}let c=Ph(a,r);if(l){let[p,h]=jh([...s],cs(l));if(!p&&!c)return[l,!1,h]}if(hr.has(a)){let p=hr.get(a);if(Dt.has(p)&&(Dt.get(p).delete(a),Dt.get(p).size===0)){Dt.delete(p);for(let[h,b]of Pi)b===p&&Pi.delete(h);for(let h of p.disposables.splice(0))h(p)}}Ze.DEBUG&&console.log("Setting up new context...");let f=il(t,[],r);Object.assign(f,{userConfigPath:i});let[,d]=jh([...s],cs(f));return Pi.set(n,f),hr.set(a,f),Dt.has(f)||Dt.set(f,new Set),Dt.get(f).add(a),[f,!0,d]}var Nh,Zo,Pt,Jo,el,rl,hr,Pi,Dt,Oi=P(()=>{u();ft();la();Ot();Nh=pe(Ra()),Zo=pe(it());Ci();qo();Gn();Kt();fr();Lo();Fr();bh();It();It();Yi();Be();Gi();Bo();os();Ih();Mh();ct();Vo();Pt=Symbol(),Jo={AddVariant:Symbol.for("ADD_VARIANT"),MatchVariant:Symbol.for("MATCH_VARIANT")},el={Base:1<<0,Dynamic:1<<1};rl=new WeakMap;hr=wh,Pi=vh,Dt=es});function nl(r){return r.ignore?[]:r.glob?m.env.ROLLUP_WATCH==="true"?[{type:"dependency",file:r.base}]:[{type:"dir-dependency",dir:r.base,glob:r.glob}]:[{type:"dependency",file:r.base}]}var Hh=P(()=>{u()});function Wh(r,e){return{handler:r,config:e}}var Gh,Qh=P(()=>{u();Wh.withOptions=function(r,e=()=>({})){let t=function(i){return{__options:i,handler:r(i),config:e(i)}};return t.__isOptionsFunction=!0,t.__pluginFunction=r,t.__configFunction=e,t};Gh=Wh});var sl={};Ge(sl,{default:()=>W_});var W_,al=P(()=>{u();Qh();W_=Gh});var Kh=x((z4,Yh)=>{u();var G_=(al(),sl).default,Q_={overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical"},Y_=G_(function({matchUtilities:r,addUtilities:e,theme:t,variants:i}){let n=t("lineClamp");r({"line-clamp":s=>({...Q_,"-webkit-line-clamp":`${s}`})},{values:n}),e([{".line-clamp-none":{"-webkit-line-clamp":"unset"}}],i("lineClamp"))},{theme:{lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"}},variants:{lineClamp:["responsive"]}});Yh.exports=Y_});function ol(r){r.content.files.length===0&&G.warn("content-problems",["The `content` option in your Tailwind CSS configuration is missing or empty.","Configure your content sources or your generated CSS will be missing styles.","https://tailwindcss.com/docs/content-configuration"]);try{let e=Kh();r.plugins.includes(e)&&(G.warn("line-clamp-in-core",["As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default.","Remove it from the `plugins` array in your configuration to eliminate this warning."]),r.plugins=r.plugins.filter(t=>t!==e))}catch{}return r}var Xh=P(()=>{u();Be()});var Zh,Jh=P(()=>{u();Zh=()=>!1});var ps,em=P(()=>{u();ps={sync:r=>[].concat(r),generateTasks:r=>[{dynamic:!1,base:".",negative:[],positive:[].concat(r),patterns:[].concat(r)}],escapePath:r=>r}});var ll,tm=P(()=>{u();ll=r=>r});var rm,im=P(()=>{u();rm=()=>""});function nm(r){let e=r,t=rm(r);return t!=="."&&(e=r.substr(t.length),e.charAt(0)==="/"&&(e=e.substr(1))),e.substr(0,2)==="./"?e=e.substr(2):e.charAt(0)==="/"&&(e=e.substr(1)),{base:t,glob:e}}var sm=P(()=>{u();im()});var ds=x(Ve=>{u();"use strict";Ve.isInteger=r=>typeof r=="number"?Number.isInteger(r):typeof r=="string"&&r.trim()!==""?Number.isInteger(Number(r)):!1;Ve.find=(r,e)=>r.nodes.find(t=>t.type===e);Ve.exceedsLimit=(r,e,t=1,i)=>i===!1||!Ve.isInteger(r)||!Ve.isInteger(e)?!1:(Number(e)-Number(r))/Number(t)>=i;Ve.escapeNode=(r,e=0,t)=>{let i=r.nodes[e];!i||(t&&i.type===t||i.type==="open"||i.type==="close")&&i.escaped!==!0&&(i.value="\\"+i.value,i.escaped=!0)};Ve.encloseBrace=r=>r.type!=="brace"?!1:r.commas>>0+r.ranges>>0==0?(r.invalid=!0,!0):!1;Ve.isInvalidBrace=r=>r.type!=="brace"?!1:r.invalid===!0||r.dollar?!0:r.commas>>0+r.ranges>>0==0||r.open!==!0||r.close!==!0?(r.invalid=!0,!0):!1;Ve.isOpenOrClose=r=>r.type==="open"||r.type==="close"?!0:r.open===!0||r.close===!0;Ve.reduce=r=>r.reduce((e,t)=>(t.type==="text"&&e.push(t.value),t.type==="range"&&(t.type="text"),e),[]);Ve.flatten=(...r)=>{let e=[],t=i=>{for(let n=0;n{u();"use strict";var am=ds();om.exports=(r,e={})=>{let t=(i,n={})=>{let s=e.escapeInvalid&&am.isInvalidBrace(n),a=i.invalid===!0&&e.escapeInvalid===!0,o="";if(i.value)return(s||a)&&am.isOpenOrClose(i)?"\\"+i.value:i.value;if(i.value)return i.value;if(i.nodes)for(let l of i.nodes)o+=t(l);return o};return t(r)}});var um=x((J4,lm)=>{u();"use strict";lm.exports=function(r){return typeof r=="number"?r-r==0:typeof r=="string"&&r.trim()!==""?Number.isFinite?Number.isFinite(+r):isFinite(+r):!1}});var bm=x((e6,ym)=>{u();"use strict";var fm=um(),Wt=(r,e,t)=>{if(fm(r)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||r===e)return String(r);if(fm(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let i={relaxZeros:!0,...t};typeof i.strictZeros=="boolean"&&(i.relaxZeros=i.strictZeros===!1);let n=String(i.relaxZeros),s=String(i.shorthand),a=String(i.capture),o=String(i.wrap),l=r+":"+e+"="+n+s+a+o;if(Wt.cache.hasOwnProperty(l))return Wt.cache[l].result;let c=Math.min(r,e),f=Math.max(r,e);if(Math.abs(c-f)===1){let v=r+"|"+e;return i.capture?`(${v})`:i.wrap===!1?v:`(?:${v})`}let d=gm(r)||gm(e),p={min:r,max:e,a:c,b:f},h=[],b=[];if(d&&(p.isPadded=d,p.maxLen=String(p.max).length),c<0){let v=f<0?Math.abs(f):1;b=cm(v,Math.abs(c),p,i),c=p.a=0}return f>=0&&(h=cm(c,f,p,i)),p.negatives=b,p.positives=h,p.result=K_(b,h,i),i.capture===!0?p.result=`(${p.result})`:i.wrap!==!1&&h.length+b.length>1&&(p.result=`(?:${p.result})`),Wt.cache[l]=p,p.result};function K_(r,e,t){let i=ul(r,e,"-",!1,t)||[],n=ul(e,r,"",!1,t)||[],s=ul(r,e,"-?",!0,t)||[];return i.concat(s).concat(n).join("|")}function X_(r,e){let t=1,i=1,n=dm(r,t),s=new Set([e]);for(;r<=n&&n<=e;)s.add(n),t+=1,n=dm(r,t);for(n=hm(e+1,i)-1;r1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+mm(o.count),a=c+1;continue}t.isPadded&&(d=rE(c,t,i)),f.string=d+f.pattern+mm(f.count),s.push(f),a=c+1,o=f}return s}function ul(r,e,t,i,n){let s=[];for(let a of r){let{string:o}=a;!i&&!pm(e,"string",o)&&s.push(t+o),i&&pm(e,"string",o)&&s.push(t+o)}return s}function J_(r,e){let t=[];for(let i=0;ie?1:e>r?-1:0}function pm(r,e,t){return r.some(i=>i[e]===t)}function dm(r,e){return Number(String(r).slice(0,-e)+"9".repeat(e))}function hm(r,e){return r-r%Math.pow(10,e)}function mm(r){let[e=0,t=""]=r;return t||e>1?`{${e+(t?","+t:"")}}`:""}function tE(r,e,t){return`[${r}${e-r==1?"":"-"}${e}]`}function gm(r){return/^-?(0+)\d/.test(r)}function rE(r,e,t){if(!e.isPadded)return r;let i=Math.abs(e.maxLen-String(r).length),n=t.relaxZeros!==!1;switch(i){case 0:return"";case 1:return n?"0?":"0";case 2:return n?"0{0,2}":"00";default:return n?`0{0,${i}}`:`0{${i}}`}}Wt.cache={};Wt.clearCache=()=>Wt.cache={};ym.exports=Wt});var pl=x((t6,Cm)=>{u();"use strict";var iE=(Fn(),Bn),wm=bm(),vm=r=>r!==null&&typeof r=="object"&&!Array.isArray(r),nE=r=>e=>r===!0?Number(e):String(e),fl=r=>typeof r=="number"||typeof r=="string"&&r!=="",Ii=r=>Number.isInteger(+r),cl=r=>{let e=`${r}`,t=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++t]==="0";);return t>0},sE=(r,e,t)=>typeof r=="string"||typeof e=="string"?!0:t.stringify===!0,aE=(r,e,t)=>{if(e>0){let i=r[0]==="-"?"-":"";i&&(r=r.slice(1)),r=i+r.padStart(i?e-1:e,"0")}return t===!1?String(r):r},ms=(r,e)=>{let t=r[0]==="-"?"-":"";for(t&&(r=r.slice(1),e--);r.length{r.negatives.sort((o,l)=>ol?1:0),r.positives.sort((o,l)=>ol?1:0);let i=e.capture?"":"?:",n="",s="",a;return r.positives.length&&(n=r.positives.map(o=>ms(String(o),t)).join("|")),r.negatives.length&&(s=`-(${i}${r.negatives.map(o=>ms(String(o),t)).join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,e.wrap?`(${i}${a})`:a},xm=(r,e,t,i)=>{if(t)return wm(r,e,{wrap:!1,...i});let n=String.fromCharCode(r);if(r===e)return n;let s=String.fromCharCode(e);return`[${n}-${s}]`},km=(r,e,t)=>{if(Array.isArray(r)){let i=t.wrap===!0,n=t.capture?"":"?:";return i?`(${n}${r.join("|")})`:r.join("|")}return wm(r,e,t)},Sm=(...r)=>new RangeError("Invalid range arguments: "+iE.inspect(...r)),Am=(r,e,t)=>{if(t.strictRanges===!0)throw Sm([r,e]);return[]},lE=(r,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${r}" to be a number`);return[]},uE=(r,e,t=1,i={})=>{let n=Number(r),s=Number(e);if(!Number.isInteger(n)||!Number.isInteger(s)){if(i.strictRanges===!0)throw Sm([r,e]);return[]}n===0&&(n=0),s===0&&(s=0);let a=n>s,o=String(r),l=String(e),c=String(t);t=Math.max(Math.abs(t),1);let f=cl(o)||cl(l)||cl(c),d=f?Math.max(o.length,l.length,c.length):0,p=f===!1&&sE(r,e,i)===!1,h=i.transform||nE(p);if(i.toRegex&&t===1)return xm(ms(r,d),ms(e,d),!0,i);let b={negatives:[],positives:[]},v=k=>b[k<0?"negatives":"positives"].push(Math.abs(k)),y=[],w=0;for(;a?n>=s:n<=s;)i.toRegex===!0&&t>1?v(n):y.push(aE(h(n,w),d,p)),n=a?n-t:n+t,w++;return i.toRegex===!0?t>1?oE(b,i,d):km(y,null,{wrap:!1,...i}):y},fE=(r,e,t=1,i={})=>{if(!Ii(r)&&r.length>1||!Ii(e)&&e.length>1)return Am(r,e,i);let n=i.transform||(p=>String.fromCharCode(p)),s=`${r}`.charCodeAt(0),a=`${e}`.charCodeAt(0),o=s>a,l=Math.min(s,a),c=Math.max(s,a);if(i.toRegex&&t===1)return xm(l,c,!1,i);let f=[],d=0;for(;o?s>=a:s<=a;)f.push(n(s,d)),s=o?s-t:s+t,d++;return i.toRegex===!0?km(f,null,{wrap:!1,options:i}):f},gs=(r,e,t,i={})=>{if(e==null&&fl(r))return[r];if(!fl(r)||!fl(e))return Am(r,e,i);if(typeof t=="function")return gs(r,e,1,{transform:t});if(vm(t))return gs(r,e,0,t);let n={...i};return n.capture===!0&&(n.wrap=!0),t=t||n.step||1,Ii(t)?Ii(r)&&Ii(e)?uE(r,e,t,n):fE(r,e,Math.max(Math.abs(t),1),n):t!=null&&!vm(t)?lE(t,n):gs(r,e,1,t)};Cm.exports=gs});var Om=x((r6,Em)=>{u();"use strict";var cE=pl(),_m=ds(),pE=(r,e={})=>{let t=(i,n={})=>{let s=_m.isInvalidBrace(n),a=i.invalid===!0&&e.escapeInvalid===!0,o=s===!0||a===!0,l=e.escapeInvalid===!0?"\\":"",c="";if(i.isOpen===!0)return l+i.value;if(i.isClose===!0)return console.log("node.isClose",l,i.value),l+i.value;if(i.type==="open")return o?l+i.value:"(";if(i.type==="close")return o?l+i.value:")";if(i.type==="comma")return i.prev.type==="comma"?"":o?i.value:"|";if(i.value)return i.value;if(i.nodes&&i.ranges>0){let f=_m.reduce(i.nodes),d=cE(...f,{...e,wrap:!1,toRegex:!0,strictZeros:!0});if(d.length!==0)return f.length>1&&d.length>1?`(${d})`:d}if(i.nodes)for(let f of i.nodes)c+=t(f,i);return c};return t(r)};Em.exports=pE});var Pm=x((i6,Rm)=>{u();"use strict";var dE=pl(),Tm=hs(),mr=ds(),Gt=(r="",e="",t=!1)=>{let i=[];if(r=[].concat(r),e=[].concat(e),!e.length)return r;if(!r.length)return t?mr.flatten(e).map(n=>`{${n}}`):e;for(let n of r)if(Array.isArray(n))for(let s of n)i.push(Gt(s,e,t));else for(let s of e)t===!0&&typeof s=="string"&&(s=`{${s}}`),i.push(Array.isArray(s)?Gt(n,s,t):n+s);return mr.flatten(i)},hE=(r,e={})=>{let t=e.rangeLimit===void 0?1e3:e.rangeLimit,i=(n,s={})=>{n.queue=[];let a=s,o=s.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,o=a.queue;if(n.invalid||n.dollar){o.push(Gt(o.pop(),Tm(n,e)));return}if(n.type==="brace"&&n.invalid!==!0&&n.nodes.length===2){o.push(Gt(o.pop(),["{}"]));return}if(n.nodes&&n.ranges>0){let d=mr.reduce(n.nodes);if(mr.exceedsLimit(...d,e.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=dE(...d,e);p.length===0&&(p=Tm(n,e)),o.push(Gt(o.pop(),p)),n.nodes=[];return}let l=mr.encloseBrace(n),c=n.queue,f=n;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,c=f.queue;for(let d=0;d{u();"use strict";Im.exports={MAX_LENGTH:1e4,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nm=x((s6,Mm)=>{u();"use strict";var mE=hs(),{MAX_LENGTH:qm,CHAR_BACKSLASH:dl,CHAR_BACKTICK:gE,CHAR_COMMA:yE,CHAR_DOT:bE,CHAR_LEFT_PARENTHESES:wE,CHAR_RIGHT_PARENTHESES:vE,CHAR_LEFT_CURLY_BRACE:xE,CHAR_RIGHT_CURLY_BRACE:kE,CHAR_LEFT_SQUARE_BRACKET:$m,CHAR_RIGHT_SQUARE_BRACKET:Lm,CHAR_DOUBLE_QUOTE:SE,CHAR_SINGLE_QUOTE:AE,CHAR_NO_BREAK_SPACE:CE,CHAR_ZERO_WIDTH_NOBREAK_SPACE:_E}=Dm(),EE=(r,e={})=>{if(typeof r!="string")throw new TypeError("Expected a string");let t=e||{},i=typeof t.maxLength=="number"?Math.min(qm,t.maxLength):qm;if(r.length>i)throw new SyntaxError(`Input length (${r.length}), exceeds max characters (${i})`);let n={type:"root",input:r,nodes:[]},s=[n],a=n,o=n,l=0,c=r.length,f=0,d=0,p,h=()=>r[f++],b=v=>{if(v.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&v.type==="text"){o.value+=v.value;return}return a.nodes.push(v),v.parent=a,v.prev=o,o=v,v};for(b({type:"bos"});f0){if(a.ranges>0){a.ranges=0;let v=a.nodes.shift();a.nodes=[v,{type:"text",value:mE(a)}]}b({type:"comma",value:p}),a.commas++;continue}if(p===bE&&d>0&&a.commas===0){let v=a.nodes;if(d===0||v.length===0){b({type:"text",value:p});continue}if(o.type==="dot"){if(a.range=[],o.value+=p,o.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,o.type="text";continue}a.ranges++,a.args=[];continue}if(o.type==="range"){v.pop();let y=v[v.length-1];y.value+=o.value+p,o=y,a.ranges--;continue}b({type:"dot",value:p});continue}b({type:"text",value:p})}do if(a=s.pop(),a.type!=="root"){a.nodes.forEach(w=>{w.nodes||(w.type==="open"&&(w.isOpen=!0),w.type==="close"&&(w.isClose=!0),w.nodes||(w.type="text"),w.invalid=!0)});let v=s[s.length-1],y=v.nodes.indexOf(a);v.nodes.splice(y,1,...a.nodes)}while(s.length>0);return b({type:"eos"}),n};Mm.exports=EE});var jm=x((a6,Fm)=>{u();"use strict";var Bm=hs(),OE=Om(),TE=Pm(),RE=Nm(),Le=(r,e={})=>{let t=[];if(Array.isArray(r))for(let i of r){let n=Le.create(i,e);Array.isArray(n)?t.push(...n):t.push(n)}else t=[].concat(Le.create(r,e));return e&&e.expand===!0&&e.nodupes===!0&&(t=[...new Set(t)]),t};Le.parse=(r,e={})=>RE(r,e);Le.stringify=(r,e={})=>typeof r=="string"?Bm(Le.parse(r,e),e):Bm(r,e);Le.compile=(r,e={})=>(typeof r=="string"&&(r=Le.parse(r,e)),OE(r,e));Le.expand=(r,e={})=>{typeof r=="string"&&(r=Le.parse(r,e));let t=TE(r,e);return e.noempty===!0&&(t=t.filter(Boolean)),e.nodupes===!0&&(t=[...new Set(t)]),t};Le.create=(r,e={})=>r===""||r.length<3?[r]:e.expand!==!0?Le.compile(r,e):Le.expand(r,e);Fm.exports=Le});var Di=x((o6,Wm)=>{u();"use strict";var PE=(et(),Ur),at="\\\\/",zm=`[^${at}]`,yt="\\.",IE="\\+",DE="\\?",ys="\\/",qE="(?=.)",Um="[^/]",hl=`(?:${ys}|$)`,Vm=`(?:^|${ys})`,ml=`${yt}{1,2}${hl}`,$E=`(?!${yt})`,LE=`(?!${Vm}${ml})`,ME=`(?!${yt}{0,1}${hl})`,NE=`(?!${ml})`,BE=`[^.${ys}]`,FE=`${Um}*?`,Hm={DOT_LITERAL:yt,PLUS_LITERAL:IE,QMARK_LITERAL:DE,SLASH_LITERAL:ys,ONE_CHAR:qE,QMARK:Um,END_ANCHOR:hl,DOTS_SLASH:ml,NO_DOT:$E,NO_DOTS:LE,NO_DOT_SLASH:ME,NO_DOTS_SLASH:NE,QMARK_NO_DOT:BE,STAR:FE,START_ANCHOR:Vm},jE={...Hm,SLASH_LITERAL:`[${at}]`,QMARK:zm,STAR:`${zm}*?`,DOTS_SLASH:`${yt}{1,2}(?:[${at}]|$)`,NO_DOT:`(?!${yt})`,NO_DOTS:`(?!(?:^|[${at}])${yt}{1,2}(?:[${at}]|$))`,NO_DOT_SLASH:`(?!${yt}{0,1}(?:[${at}]|$))`,NO_DOTS_SLASH:`(?!${yt}{1,2}(?:[${at}]|$))`,QMARK_NO_DOT:`[^.${at}]`,START_ANCHOR:`(?:^|[${at}])`,END_ANCHOR:`(?:[${at}]|$)`},zE={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Wm.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:zE,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:PE.sep,extglobChars(r){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${r.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(r){return r===!0?jE:Hm}}});var qi=x(Re=>{u();"use strict";var UE=(et(),Ur),VE=m.platform==="win32",{REGEX_BACKSLASH:HE,REGEX_REMOVE_BACKSLASH:WE,REGEX_SPECIAL_CHARS:GE,REGEX_SPECIAL_CHARS_GLOBAL:QE}=Di();Re.isObject=r=>r!==null&&typeof r=="object"&&!Array.isArray(r);Re.hasRegexChars=r=>GE.test(r);Re.isRegexChar=r=>r.length===1&&Re.hasRegexChars(r);Re.escapeRegex=r=>r.replace(QE,"\\$1");Re.toPosixSlashes=r=>r.replace(HE,"/");Re.removeBackslashes=r=>r.replace(WE,e=>e==="\\"?"":e);Re.supportsLookbehinds=()=>{let r=m.version.slice(1).split(".").map(Number);return r.length===3&&r[0]>=9||r[0]===8&&r[1]>=10};Re.isWindows=r=>r&&typeof r.windows=="boolean"?r.windows:VE===!0||UE.sep==="\\";Re.escapeLast=(r,e,t)=>{let i=r.lastIndexOf(e,t);return i===-1?r:r[i-1]==="\\"?Re.escapeLast(r,e,i-1):`${r.slice(0,i)}\\${r.slice(i)}`};Re.removePrefix=(r,e={})=>{let t=r;return t.startsWith("./")&&(t=t.slice(2),e.prefix="./"),t};Re.wrapOutput=(r,e={},t={})=>{let i=t.contains?"":"^",n=t.contains?"":"$",s=`${i}(?:${r})${n}`;return e.negated===!0&&(s=`(?:^(?!${s}).*$)`),s}});var eg=x((u6,Jm)=>{u();"use strict";var Gm=qi(),{CHAR_ASTERISK:gl,CHAR_AT:YE,CHAR_BACKWARD_SLASH:$i,CHAR_COMMA:KE,CHAR_DOT:yl,CHAR_EXCLAMATION_MARK:bl,CHAR_FORWARD_SLASH:Qm,CHAR_LEFT_CURLY_BRACE:wl,CHAR_LEFT_PARENTHESES:vl,CHAR_LEFT_SQUARE_BRACKET:XE,CHAR_PLUS:ZE,CHAR_QUESTION_MARK:Ym,CHAR_RIGHT_CURLY_BRACE:JE,CHAR_RIGHT_PARENTHESES:Km,CHAR_RIGHT_SQUARE_BRACKET:e2}=Di(),Xm=r=>r===Qm||r===$i,Zm=r=>{r.isPrefix!==!0&&(r.depth=r.isGlobstar?1/0:1)},t2=(r,e)=>{let t=e||{},i=r.length-1,n=t.parts===!0||t.scanToEnd===!0,s=[],a=[],o=[],l=r,c=-1,f=0,d=0,p=!1,h=!1,b=!1,v=!1,y=!1,w=!1,k=!1,S=!1,E=!1,T=!1,B=0,N,R,F={value:"",depth:0,isGlob:!1},Y=()=>c>=i,_=()=>l.charCodeAt(c+1),Q=()=>(N=R,l.charCodeAt(++c));for(;c0&&(le=l.slice(0,f),l=l.slice(f),d-=f),U&&b===!0&&d>0?(U=l.slice(0,d),A=l.slice(d)):b===!0?(U="",A=l):U=l,U&&U!==""&&U!=="/"&&U!==l&&Xm(U.charCodeAt(U.length-1))&&(U=U.slice(0,-1)),t.unescape===!0&&(A&&(A=Gm.removeBackslashes(A)),U&&k===!0&&(U=Gm.removeBackslashes(U)));let C={prefix:le,input:r,start:f,base:U,glob:A,isBrace:p,isBracket:h,isGlob:b,isExtglob:v,isGlobstar:y,negated:S,negatedExtglob:E};if(t.tokens===!0&&(C.maxDepth=0,Xm(R)||a.push(F),C.tokens=a),t.parts===!0||t.tokens===!0){let he;for(let V=0;V{u();"use strict";var bs=Di(),Me=qi(),{MAX_LENGTH:ws,POSIX_REGEX_SOURCE:r2,REGEX_NON_SPECIAL_CHARS:i2,REGEX_SPECIAL_CHARS_BACKREF:n2,REPLACEMENTS:tg}=bs,s2=(r,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...r,e);r.sort();let t=`[${r.join("-")}]`;try{new RegExp(t)}catch(i){return r.map(n=>Me.escapeRegex(n)).join("..")}return t},gr=(r,e)=>`Missing ${r}: "${e}" - use "\\\\${e}" to match literal characters`,xl=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");r=tg[r]||r;let t={...e},i=typeof t.maxLength=="number"?Math.min(ws,t.maxLength):ws,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);let s={type:"bos",value:"",output:t.prepend||""},a=[s],o=t.capture?"":"?:",l=Me.isWindows(e),c=bs.globChars(l),f=bs.extglobChars(c),{DOT_LITERAL:d,PLUS_LITERAL:p,SLASH_LITERAL:h,ONE_CHAR:b,DOTS_SLASH:v,NO_DOT:y,NO_DOT_SLASH:w,NO_DOTS_SLASH:k,QMARK:S,QMARK_NO_DOT:E,STAR:T,START_ANCHOR:B}=c,N=$=>`(${o}(?:(?!${B}${$.dot?v:d}).)*?)`,R=t.dot?"":y,F=t.dot?S:E,Y=t.bash===!0?N(t):T;t.capture&&(Y=`(${Y})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let _={input:r,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};r=Me.removePrefix(r,_),n=r.length;let Q=[],U=[],le=[],A=s,C,he=()=>_.index===n-1,V=_.peek=($=1)=>r[_.index+$],Ee=_.advance=()=>r[++_.index]||"",Ie=()=>r.slice(_.index+1),De=($="",ae=0)=>{_.consumed+=$,_.index+=ae},ji=$=>{_.output+=$.output!=null?$.output:$.value,De($.value)},Iv=()=>{let $=1;for(;V()==="!"&&(V(2)!=="("||V(3)==="?");)Ee(),_.start++,$++;return $%2==0?!1:(_.negated=!0,_.start++,!0)},zi=$=>{_[$]++,le.push($)},Ft=$=>{_[$]--,le.pop()},W=$=>{if(A.type==="globstar"){let ae=_.braces>0&&($.type==="comma"||$.type==="brace"),I=$.extglob===!0||Q.length&&($.type==="pipe"||$.type==="paren");$.type!=="slash"&&$.type!=="paren"&&!ae&&!I&&(_.output=_.output.slice(0,-A.output.length),A.type="star",A.value="*",A.output=Y,_.output+=A.output)}if(Q.length&&$.type!=="paren"&&(Q[Q.length-1].inner+=$.value),($.value||$.output)&&ji($),A&&A.type==="text"&&$.type==="text"){A.value+=$.value,A.output=(A.output||"")+$.value;return}$.prev=A,a.push($),A=$},Ui=($,ae)=>{let I={...f[ae],conditions:1,inner:""};I.prev=A,I.parens=_.parens,I.output=_.output;let H=(t.capture?"(":"")+I.open;zi("parens"),W({type:$,value:ae,output:_.output?"":b}),W({type:"paren",extglob:!0,value:Ee(),output:H}),Q.push(I)},Dv=$=>{let ae=$.close+(t.capture?")":""),I;if($.type==="negate"){let H=Y;if($.inner&&$.inner.length>1&&$.inner.includes("/")&&(H=N(t)),(H!==Y||he()||/^\)+$/.test(Ie()))&&(ae=$.close=`)$))${H}`),$.inner.includes("*")&&(I=Ie())&&/^\.[^\\/.]+$/.test(I)){let ce=xl(I,{...e,fastpaths:!1}).output;ae=$.close=`)${ce})${H})`}$.prev.type==="bos"&&(_.negatedExtglob=!0)}W({type:"paren",extglob:!0,value:C,output:ae}),Ft("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(r)){let $=!1,ae=r.replace(n2,(I,H,ce,Ce,ye,Bs)=>Ce==="\\"?($=!0,I):Ce==="?"?H?H+Ce+(ye?S.repeat(ye.length):""):Bs===0?F+(ye?S.repeat(ye.length):""):S.repeat(ce.length):Ce==="."?d.repeat(ce.length):Ce==="*"?H?H+Ce+(ye?Y:""):Y:H?I:`\\${I}`);return $===!0&&(t.unescape===!0?ae=ae.replace(/\\/g,""):ae=ae.replace(/\\+/g,I=>I.length%2==0?"\\\\":I?"\\":"")),ae===r&&t.contains===!0?(_.output=r,_):(_.output=Me.wrapOutput(ae,_,e),_)}for(;!he();){if(C=Ee(),C==="\0")continue;if(C==="\\"){let I=V();if(I==="/"&&t.bash!==!0||I==="."||I===";")continue;if(!I){C+="\\",W({type:"text",value:C});continue}let H=/^\\+/.exec(Ie()),ce=0;if(H&&H[0].length>2&&(ce=H[0].length,_.index+=ce,ce%2!=0&&(C+="\\")),t.unescape===!0?C=Ee():C+=Ee(),_.brackets===0){W({type:"text",value:C});continue}}if(_.brackets>0&&(C!=="]"||A.value==="["||A.value==="[^")){if(t.posix!==!1&&C===":"){let I=A.value.slice(1);if(I.includes("[")&&(A.posix=!0,I.includes(":"))){let H=A.value.lastIndexOf("["),ce=A.value.slice(0,H),Ce=A.value.slice(H+2),ye=r2[Ce];if(ye){A.value=ce+ye,_.backtrack=!0,Ee(),!s.output&&a.indexOf(A)===1&&(s.output=b);continue}}}(C==="["&&V()!==":"||C==="-"&&V()==="]")&&(C=`\\${C}`),C==="]"&&(A.value==="["||A.value==="[^")&&(C=`\\${C}`),t.posix===!0&&C==="!"&&A.value==="["&&(C="^"),A.value+=C,ji({value:C});continue}if(_.quotes===1&&C!=='"'){C=Me.escapeRegex(C),A.value+=C,ji({value:C});continue}if(C==='"'){_.quotes=_.quotes===1?0:1,t.keepQuotes===!0&&W({type:"text",value:C});continue}if(C==="("){zi("parens"),W({type:"paren",value:C});continue}if(C===")"){if(_.parens===0&&t.strictBrackets===!0)throw new SyntaxError(gr("opening","("));let I=Q[Q.length-1];if(I&&_.parens===I.parens+1){Dv(Q.pop());continue}W({type:"paren",value:C,output:_.parens?")":"\\)"}),Ft("parens");continue}if(C==="["){if(t.nobracket===!0||!Ie().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(gr("closing","]"));C=`\\${C}`}else zi("brackets");W({type:"bracket",value:C});continue}if(C==="]"){if(t.nobracket===!0||A&&A.type==="bracket"&&A.value.length===1){W({type:"text",value:C,output:`\\${C}`});continue}if(_.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(gr("opening","["));W({type:"text",value:C,output:`\\${C}`});continue}Ft("brackets");let I=A.value.slice(1);if(A.posix!==!0&&I[0]==="^"&&!I.includes("/")&&(C=`/${C}`),A.value+=C,ji({value:C}),t.literalBrackets===!1||Me.hasRegexChars(I))continue;let H=Me.escapeRegex(A.value);if(_.output=_.output.slice(0,-A.value.length),t.literalBrackets===!0){_.output+=H,A.value=H;continue}A.value=`(${o}${H}|${A.value})`,_.output+=A.value;continue}if(C==="{"&&t.nobrace!==!0){zi("braces");let I={type:"brace",value:C,output:"(",outputIndex:_.output.length,tokensIndex:_.tokens.length};U.push(I),W(I);continue}if(C==="}"){let I=U[U.length-1];if(t.nobrace===!0||!I){W({type:"text",value:C,output:C});continue}let H=")";if(I.dots===!0){let ce=a.slice(),Ce=[];for(let ye=ce.length-1;ye>=0&&(a.pop(),ce[ye].type!=="brace");ye--)ce[ye].type!=="dots"&&Ce.unshift(ce[ye].value);H=s2(Ce,t),_.backtrack=!0}if(I.comma!==!0&&I.dots!==!0){let ce=_.output.slice(0,I.outputIndex),Ce=_.tokens.slice(I.tokensIndex);I.value=I.output="\\{",C=H="\\}",_.output=ce;for(let ye of Ce)_.output+=ye.output||ye.value}W({type:"brace",value:C,output:H}),Ft("braces"),U.pop();continue}if(C==="|"){Q.length>0&&Q[Q.length-1].conditions++,W({type:"text",value:C});continue}if(C===","){let I=C,H=U[U.length-1];H&&le[le.length-1]==="braces"&&(H.comma=!0,I="|"),W({type:"comma",value:C,output:I});continue}if(C==="/"){if(A.type==="dot"&&_.index===_.start+1){_.start=_.index+1,_.consumed="",_.output="",a.pop(),A=s;continue}W({type:"slash",value:C,output:h});continue}if(C==="."){if(_.braces>0&&A.type==="dot"){A.value==="."&&(A.output=d);let I=U[U.length-1];A.type="dots",A.output+=C,A.value+=C,I.dots=!0;continue}if(_.braces+_.parens===0&&A.type!=="bos"&&A.type!=="slash"){W({type:"text",value:C,output:d});continue}W({type:"dot",value:C,output:d});continue}if(C==="?"){if(!(A&&A.value==="(")&&t.noextglob!==!0&&V()==="("&&V(2)!=="?"){Ui("qmark",C);continue}if(A&&A.type==="paren"){let H=V(),ce=C;if(H==="<"&&!Me.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(A.value==="("&&!/[!=<:]/.test(H)||H==="<"&&!/<([!=]|\w+>)/.test(Ie()))&&(ce=`\\${C}`),W({type:"text",value:C,output:ce});continue}if(t.dot!==!0&&(A.type==="slash"||A.type==="bos")){W({type:"qmark",value:C,output:E});continue}W({type:"qmark",value:C,output:S});continue}if(C==="!"){if(t.noextglob!==!0&&V()==="("&&(V(2)!=="?"||!/[!=<:]/.test(V(3)))){Ui("negate",C);continue}if(t.nonegate!==!0&&_.index===0){Iv();continue}}if(C==="+"){if(t.noextglob!==!0&&V()==="("&&V(2)!=="?"){Ui("plus",C);continue}if(A&&A.value==="("||t.regex===!1){W({type:"plus",value:C,output:p});continue}if(A&&(A.type==="bracket"||A.type==="paren"||A.type==="brace")||_.parens>0){W({type:"plus",value:C});continue}W({type:"plus",value:p});continue}if(C==="@"){if(t.noextglob!==!0&&V()==="("&&V(2)!=="?"){W({type:"at",extglob:!0,value:C,output:""});continue}W({type:"text",value:C});continue}if(C!=="*"){(C==="$"||C==="^")&&(C=`\\${C}`);let I=i2.exec(Ie());I&&(C+=I[0],_.index+=I[0].length),W({type:"text",value:C});continue}if(A&&(A.type==="globstar"||A.star===!0)){A.type="star",A.star=!0,A.value+=C,A.output=Y,_.backtrack=!0,_.globstar=!0,De(C);continue}let $=Ie();if(t.noextglob!==!0&&/^\([^?]/.test($)){Ui("star",C);continue}if(A.type==="star"){if(t.noglobstar===!0){De(C);continue}let I=A.prev,H=I.prev,ce=I.type==="slash"||I.type==="bos",Ce=H&&(H.type==="star"||H.type==="globstar");if(t.bash===!0&&(!ce||$[0]&&$[0]!=="/")){W({type:"star",value:C,output:""});continue}let ye=_.braces>0&&(I.type==="comma"||I.type==="brace"),Bs=Q.length&&(I.type==="pipe"||I.type==="paren");if(!ce&&I.type!=="paren"&&!ye&&!Bs){W({type:"star",value:C,output:""});continue}for(;$.slice(0,3)==="/**";){let Vi=r[_.index+4];if(Vi&&Vi!=="/")break;$=$.slice(3),De("/**",3)}if(I.type==="bos"&&he()){A.type="globstar",A.value+=C,A.output=N(t),_.output=A.output,_.globstar=!0,De(C);continue}if(I.type==="slash"&&I.prev.type!=="bos"&&!Ce&&he()){_.output=_.output.slice(0,-(I.output+A.output).length),I.output=`(?:${I.output}`,A.type="globstar",A.output=N(t)+(t.strictSlashes?")":"|$)"),A.value+=C,_.globstar=!0,_.output+=I.output+A.output,De(C);continue}if(I.type==="slash"&&I.prev.type!=="bos"&&$[0]==="/"){let Vi=$[1]!==void 0?"|$":"";_.output=_.output.slice(0,-(I.output+A.output).length),I.output=`(?:${I.output}`,A.type="globstar",A.output=`${N(t)}${h}|${h}${Vi})`,A.value+=C,_.output+=I.output+A.output,_.globstar=!0,De(C+Ee()),W({type:"slash",value:"/",output:""});continue}if(I.type==="bos"&&$[0]==="/"){A.type="globstar",A.value+=C,A.output=`(?:^|${h}|${N(t)}${h})`,_.output=A.output,_.globstar=!0,De(C+Ee()),W({type:"slash",value:"/",output:""});continue}_.output=_.output.slice(0,-A.output.length),A.type="globstar",A.output=N(t),A.value+=C,_.output+=A.output,_.globstar=!0,De(C);continue}let ae={type:"star",value:C,output:Y};if(t.bash===!0){ae.output=".*?",(A.type==="bos"||A.type==="slash")&&(ae.output=R+ae.output),W(ae);continue}if(A&&(A.type==="bracket"||A.type==="paren")&&t.regex===!0){ae.output=C,W(ae);continue}(_.index===_.start||A.type==="slash"||A.type==="dot")&&(A.type==="dot"?(_.output+=w,A.output+=w):t.dot===!0?(_.output+=k,A.output+=k):(_.output+=R,A.output+=R),V()!=="*"&&(_.output+=b,A.output+=b)),W(ae)}for(;_.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing","]"));_.output=Me.escapeLast(_.output,"["),Ft("brackets")}for(;_.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing",")"));_.output=Me.escapeLast(_.output,"("),Ft("parens")}for(;_.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing","}"));_.output=Me.escapeLast(_.output,"{"),Ft("braces")}if(t.strictSlashes!==!0&&(A.type==="star"||A.type==="bracket")&&W({type:"maybe_slash",value:"",output:`${h}?`}),_.backtrack===!0){_.output="";for(let $ of _.tokens)_.output+=$.output!=null?$.output:$.value,$.suffix&&(_.output+=$.suffix)}return _};xl.fastpaths=(r,e)=>{let t={...e},i=typeof t.maxLength=="number"?Math.min(ws,t.maxLength):ws,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);r=tg[r]||r;let s=Me.isWindows(e),{DOT_LITERAL:a,SLASH_LITERAL:o,ONE_CHAR:l,DOTS_SLASH:c,NO_DOT:f,NO_DOTS:d,NO_DOTS_SLASH:p,STAR:h,START_ANCHOR:b}=bs.globChars(s),v=t.dot?d:f,y=t.dot?p:f,w=t.capture?"":"?:",k={negated:!1,prefix:""},S=t.bash===!0?".*?":h;t.capture&&(S=`(${S})`);let E=R=>R.noglobstar===!0?S:`(${w}(?:(?!${b}${R.dot?c:a}).)*?)`,T=R=>{switch(R){case"*":return`${v}${l}${S}`;case".*":return`${a}${l}${S}`;case"*.*":return`${v}${S}${a}${l}${S}`;case"*/*":return`${v}${S}${o}${l}${y}${S}`;case"**":return v+E(t);case"**/*":return`(?:${v}${E(t)}${o})?${y}${l}${S}`;case"**/*.*":return`(?:${v}${E(t)}${o})?${y}${S}${a}${l}${S}`;case"**/.*":return`(?:${v}${E(t)}${o})?${a}${l}${S}`;default:{let F=/^(.*?)\.(\w+)$/.exec(R);if(!F)return;let Y=T(F[1]);return Y?Y+a+F[2]:void 0}}},B=Me.removePrefix(r,k),N=T(B);return N&&t.strictSlashes!==!0&&(N+=`${o}?`),N};rg.exports=xl});var sg=x((c6,ng)=>{u();"use strict";var a2=(et(),Ur),o2=eg(),kl=ig(),Sl=qi(),l2=Di(),u2=r=>r&&typeof r=="object"&&!Array.isArray(r),de=(r,e,t=!1)=>{if(Array.isArray(r)){let f=r.map(p=>de(p,e,t));return p=>{for(let h of f){let b=h(p);if(b)return b}return!1}}let i=u2(r)&&r.tokens&&r.input;if(r===""||typeof r!="string"&&!i)throw new TypeError("Expected pattern to be a non-empty string");let n=e||{},s=Sl.isWindows(e),a=i?de.compileRe(r,e):de.makeRe(r,e,!1,!0),o=a.state;delete a.state;let l=()=>!1;if(n.ignore){let f={...e,ignore:null,onMatch:null,onResult:null};l=de(n.ignore,f,t)}let c=(f,d=!1)=>{let{isMatch:p,match:h,output:b}=de.test(f,a,e,{glob:r,posix:s}),v={glob:r,state:o,regex:a,posix:s,input:f,output:b,match:h,isMatch:p};return typeof n.onResult=="function"&&n.onResult(v),p===!1?(v.isMatch=!1,d?v:!1):l(f)?(typeof n.onIgnore=="function"&&n.onIgnore(v),v.isMatch=!1,d?v:!1):(typeof n.onMatch=="function"&&n.onMatch(v),d?v:!0)};return t&&(c.state=o),c};de.test=(r,e,t,{glob:i,posix:n}={})=>{if(typeof r!="string")throw new TypeError("Expected input to be a string");if(r==="")return{isMatch:!1,output:""};let s=t||{},a=s.format||(n?Sl.toPosixSlashes:null),o=r===i,l=o&&a?a(r):r;return o===!1&&(l=a?a(r):r,o=l===i),(o===!1||s.capture===!0)&&(s.matchBase===!0||s.basename===!0?o=de.matchBase(r,e,t,n):o=e.exec(l)),{isMatch:Boolean(o),match:o,output:l}};de.matchBase=(r,e,t,i=Sl.isWindows(t))=>(e instanceof RegExp?e:de.makeRe(e,t)).test(a2.basename(r));de.isMatch=(r,e,t)=>de(e,t)(r);de.parse=(r,e)=>Array.isArray(r)?r.map(t=>de.parse(t,e)):kl(r,{...e,fastpaths:!1});de.scan=(r,e)=>o2(r,e);de.compileRe=(r,e,t=!1,i=!1)=>{if(t===!0)return r.output;let n=e||{},s=n.contains?"":"^",a=n.contains?"":"$",o=`${s}(?:${r.output})${a}`;r&&r.negated===!0&&(o=`^(?!${o}).*$`);let l=de.toRegex(o,e);return i===!0&&(l.state=r),l};de.makeRe=(r,e={},t=!1,i=!1)=>{if(!r||typeof r!="string")throw new TypeError("Expected a non-empty string");let n={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(r[0]==="."||r[0]==="*")&&(n.output=kl.fastpaths(r,e)),n.output||(n=kl(r,e)),de.compileRe(n,e,t,i)};de.toRegex=(r,e)=>{try{let t=e||{};return new RegExp(r,t.flags||(t.nocase?"i":""))}catch(t){if(e&&e.debug===!0)throw t;return/$^/}};de.constants=l2;ng.exports=de});var og=x((p6,ag)=>{u();"use strict";ag.exports=sg()});var dg=x((d6,pg)=>{u();"use strict";var lg=(Fn(),Bn),ug=jm(),ot=og(),Al=qi(),fg=r=>r===""||r==="./",cg=r=>{let e=r.indexOf("{");return e>-1&&r.indexOf("}",e)>-1},oe=(r,e,t)=>{e=[].concat(e),r=[].concat(r);let i=new Set,n=new Set,s=new Set,a=0,o=f=>{s.add(f.output),t&&t.onResult&&t.onResult(f)};for(let f=0;f!i.has(f));if(t&&c.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?e.map(f=>f.replace(/\\/g,"")):e}return c};oe.match=oe;oe.matcher=(r,e)=>ot(r,e);oe.isMatch=(r,e,t)=>ot(e,t)(r);oe.any=oe.isMatch;oe.not=(r,e,t={})=>{e=[].concat(e).map(String);let i=new Set,n=[],s=o=>{t.onResult&&t.onResult(o),n.push(o.output)},a=new Set(oe(r,e,{...t,onResult:s}));for(let o of n)a.has(o)||i.add(o);return[...i]};oe.contains=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${lg.inspect(r)}"`);if(Array.isArray(e))return e.some(i=>oe.contains(r,i,t));if(typeof e=="string"){if(fg(r)||fg(e))return!1;if(r.includes(e)||r.startsWith("./")&&r.slice(2).includes(e))return!0}return oe.isMatch(r,e,{...t,contains:!0})};oe.matchKeys=(r,e,t)=>{if(!Al.isObject(r))throw new TypeError("Expected the first argument to be an object");let i=oe(Object.keys(r),e,t),n={};for(let s of i)n[s]=r[s];return n};oe.some=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=ot(String(n),t);if(i.some(a=>s(a)))return!0}return!1};oe.every=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=ot(String(n),t);if(!i.every(a=>s(a)))return!1}return!0};oe.all=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${lg.inspect(r)}"`);return[].concat(e).every(i=>ot(i,t)(r))};oe.capture=(r,e,t)=>{let i=Al.isWindows(t),s=ot.makeRe(String(r),{...t,capture:!0}).exec(i?Al.toPosixSlashes(e):e);if(s)return s.slice(1).map(a=>a===void 0?"":a)};oe.makeRe=(...r)=>ot.makeRe(...r);oe.scan=(...r)=>ot.scan(...r);oe.parse=(r,e)=>{let t=[];for(let i of[].concat(r||[]))for(let n of ug(String(i),e))t.push(ot.parse(n,e));return t};oe.braces=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!cg(r)?[r]:ug(r,e)};oe.braceExpand=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return oe.braces(r,{...e,expand:!0})};oe.hasBraces=cg;pg.exports=oe});function mg(r,e){let t=e.content.files;t=t.filter(o=>typeof o=="string"),t=t.map(ll);let i=ps.generateTasks(t),n=[],s=[];for(let o of i)n.push(...o.positive.map(l=>gg(l,!1))),s.push(...o.negative.map(l=>gg(l,!0)));let a=[...n,...s];return a=c2(r,a),a=a.flatMap(p2),a=a.map(f2),a}function gg(r,e){let t={original:r,base:r,ignore:e,pattern:r,glob:null};return Zh(r)&&Object.assign(t,nm(r)),t}function f2(r){let e=ll(r.base);return e=ps.escapePath(e),r.pattern=r.glob?`${e}/${r.glob}`:e,r.pattern=r.ignore?`!${r.pattern}`:r.pattern,r}function c2(r,e){let t=[];return r.userConfigPath&&r.tailwindConfig.content.relative&&(t=[me.dirname(r.userConfigPath)]),e.map(i=>(i.base=me.resolve(...t,i.base),i))}function p2(r){let e=[r];try{let t=be.realpathSync(r.base);t!==r.base&&e.push({...r,base:t})}catch{}return e}function yg(r,e,t){let i=r.tailwindConfig.content.files.filter(a=>typeof a.raw=="string").map(({raw:a,extension:o="html"})=>({content:a,extension:o})),[n,s]=h2(e,t);for(let a of n){let o=me.extname(a).slice(1);i.push({file:a,extension:o})}return[i,s]}function d2(r){if(!r.some(s=>s.includes("**")&&!wg.test(s)))return()=>{};let t=[],i=[];for(let s of r){let a=hg.default.matcher(s);wg.test(s)&&i.push(a),t.push(a)}let n=!1;return s=>{if(n||i.some(f=>f(s)))return;let a=t.findIndex(f=>f(s));if(a===-1)return;let o=r[a],l=me.relative(m.cwd(),o);l[0]!=="."&&(l=`./${l}`);let c=bg.find(f=>s.includes(f));c&&(n=!0,G.warn("broad-content-glob-pattern",[`Your \`content\` configuration includes a pattern which looks like it's accidentally matching all of \`${c}\` and can cause serious performance issues.`,`Pattern: \`${l}\``,"See our documentation for recommendations:","https://tailwindcss.com/docs/content-configuration#pattern-recommendations"]))}}function h2(r,e){let t=r.map(o=>o.pattern),i=new Map,n=d2(t),s=new Set;Ze.DEBUG&&console.time("Finding changed files");let a=ps.sync(t,{absolute:!0});for(let o of a){n(o);let l=e.get(o)||-1/0,c=be.statSync(o).mtimeMs;c>l&&(s.add(o),i.set(o,c))}return Ze.DEBUG&&console.timeEnd("Finding changed files"),[s,i]}var hg,bg,wg,vg=P(()=>{u();ft();et();Jh();em();tm();sm();It();Be();hg=pe(dg());bg=["node_modules"],wg=new RegExp(`(${bg.map(r=>String.raw`\b${r}\b`).join("|")})`)});function xg(){}var kg=P(()=>{u()});function b2(r,e){for(let t of e){let i=`${r}${t}`;if(be.existsSync(i)&&be.statSync(i).isFile())return i}for(let t of e){let i=`${r}/index${t}`;if(be.existsSync(i))return i}return null}function*Sg(r,e,t,i=me.extname(r)){let n=b2(me.resolve(e,r),m2.includes(i)?g2:y2);if(n===null||t.has(n))return;t.add(n),yield n,e=me.dirname(n),i=me.extname(n);let s=be.readFileSync(n,"utf-8");for(let a of[...s.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),...s.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),...s.matchAll(/require\(['"`](.+)['"`]\)/gi)])!a[1].startsWith(".")||(yield*Sg(a[1],e,t,i))}function Cl(r){return r===null?new Set:new Set(Sg(r,me.dirname(r),new Set))}var m2,g2,y2,Ag=P(()=>{u();ft();et();m2=[".js",".cjs",".mjs"],g2=["",".js",".cjs",".mjs",".ts",".cts",".mts",".jsx",".tsx"],y2=["",".ts",".cts",".mts",".tsx",".js",".cjs",".mjs",".jsx"]});function w2(r,e){if(_l.has(r))return _l.get(r);let t=mg(r,e);return _l.set(r,t).get(r)}function v2(r){let e=aa(r);if(e!==null){let[i,n,s,a]=_g.get(e)||[],o=Cl(e),l=!1,c=new Map;for(let p of o){let h=be.statSync(p).mtimeMs;c.set(p,h),(!a||!a.has(p)||h>a.get(p))&&(l=!0)}if(!l)return[i,e,n,s];for(let p of o)delete hf.cache[p];let f=ol(zr(xg(e))),d=Wi(f);return _g.set(e,[f,d,o,c]),[f,e,d,o]}let t=zr(r?.config??r??{});return t=ol(t),[t,null,Wi(t),[]]}function El(r){return({tailwindDirectives:e,registerDependency:t})=>(i,n)=>{let[s,a,o,l]=v2(r),c=new Set(l);if(e.size>0){c.add(n.opts.from);for(let b of n.messages)b.type==="dependency"&&c.add(b.file)}let[f,,d]=Vh(i,n,s,a,o,c),p=cs(f),h=w2(f,s);if(e.size>0){for(let y of h)for(let w of nl(y))t(w);let[b,v]=yg(f,h,p);for(let y of b)f.changedContent.push(y);for(let[y,w]of v.entries())d.set(y,w)}for(let b of l)t({type:"dependency",file:b});for(let[b,v]of d.entries())p.set(b,v);return f}}var Cg,_g,_l,Eg=P(()=>{u();ft();Cg=pe(Fs());wf();sa();oc();Oi();Hh();Xh();vg();kg();Ag();_g=new Cg.default({maxSize:100}),_l=new WeakMap});function Ol(r){let e=new Set,t=new Set,i=new Set;if(r.walkAtRules(n=>{n.name==="apply"&&i.add(n),n.name==="import"&&(n.params==='"tailwindcss/base"'||n.params==="'tailwindcss/base'"?(n.name="tailwind",n.params="base"):n.params==='"tailwindcss/components"'||n.params==="'tailwindcss/components'"?(n.name="tailwind",n.params="components"):n.params==='"tailwindcss/utilities"'||n.params==="'tailwindcss/utilities'"?(n.name="tailwind",n.params="utilities"):(n.params==='"tailwindcss/screens"'||n.params==="'tailwindcss/screens'"||n.params==='"tailwindcss/variants"'||n.params==="'tailwindcss/variants'")&&(n.name="tailwind",n.params="variants")),n.name==="tailwind"&&(n.params==="screens"&&(n.params="variants"),e.add(n.params)),["layer","responsive","variants"].includes(n.name)&&(["responsive","variants"].includes(n.name)&&G.warn(`${n.name}-at-rule-deprecated`,[`The \`@${n.name}\` directive has been deprecated in Tailwind CSS v3.0.`,"Use `@layer utilities` or `@layer components` instead.","https://tailwindcss.com/docs/upgrade-guide#replace-variants-with-layer"]),t.add(n))}),!e.has("base")||!e.has("components")||!e.has("utilities")){for(let n of t)if(n.name==="layer"&&["base","components","utilities"].includes(n.params)){if(!e.has(n.params))throw n.error(`\`@layer ${n.params}\` is used but no matching \`@tailwind ${n.params}\` directive is present.`)}else if(n.name==="responsive"){if(!e.has("utilities"))throw n.error("`@responsive` is used but `@tailwind utilities` is missing.")}else if(n.name==="variants"&&!e.has("utilities"))throw n.error("`@variants` is used but `@tailwind utilities` is missing.")}return{tailwindDirectives:e,applyDirectives:i}}var Og=P(()=>{u();Be()});function Qt(r,e=void 0,t=void 0){return r.map(i=>{let n=i.clone();return t!==void 0&&(n.raws.tailwind={...n.raws.tailwind,...t}),e!==void 0&&Tg(n,s=>{if(s.raws.tailwind?.preserveSource===!0&&s.source)return!1;s.source=e}),n})}function Tg(r,e){e(r)!==!1&&r.each?.(t=>Tg(t,e))}var Rg=P(()=>{u()});function Tl(r){return r=Array.isArray(r)?r:[r],r=r.map(e=>e instanceof RegExp?e.source:e),r.join("")}function Ne(r){return new RegExp(Tl(r),"g")}function qt(r){return`(?:${r.map(Tl).join("|")})`}function Rl(r){return`(?:${Tl(r)})?`}function Ig(r){return r&&x2.test(r)?r.replace(Pg,"\\$&"):r||""}var Pg,x2,Dg=P(()=>{u();Pg=/[\\^$.*+?()[\]{}|]/g,x2=RegExp(Pg.source)});function qg(r){let e=Array.from(k2(r));return t=>{let i=[];for(let n of e)for(let s of t.match(n)??[])i.push(C2(s));for(let n of i.slice()){let s=ve(n,".");for(let a=0;a=s.length-1){i.push(o);continue}let l=Number(s[a+1]);isNaN(l)?i.push(o):a++}}return i}}function*k2(r){let e=r.tailwindConfig.separator,t=r.tailwindConfig.prefix!==""?Rl(Ne([/-?/,Ig(r.tailwindConfig.prefix)])):"",i=qt([/\[[^\s:'"`]+:[^\s\[\]]+\]/,/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,Ne([qt([/-?(?:\w+)/,/@(?:\w+)/]),Rl(qt([Ne([qt([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s:\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\><$]*)?/]),Ne([qt([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\$]*)?/]),/[-\/][^\s'"`\\$={><]*/]))])]),n=[qt([Ne([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/[\w_-]+/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/,e]),Ne([/[^\s"'`\[\\]+/,e])]),qt([Ne([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/[\w_-]+/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/,e]),Ne([/[^\s`\[\\]+/,e])])];for(let s of n)yield Ne(["((?=((",s,")+))\\2)?",/!?/,t,i]);yield/[^<>"'`\s.(){}[\]#=%$][^<>"'`\s(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g}function C2(r){if(!r.includes("-["))return r;let e=0,t=[],i=r.matchAll(S2);i=Array.from(i).flatMap(n=>{let[,...s]=n;return s.map((a,o)=>Object.assign([],n,{index:n.index+o,0:a}))});for(let n of i){let s=n[0],a=t[t.length-1];if(s===a?t.pop():(s==="'"||s==='"'||s==="`")&&t.push(s),!a){if(s==="["){e++;continue}else if(s==="]"){e--;continue}if(e<0)return r.substring(0,n.index-1);if(e===0&&!A2.test(s))return r.substring(0,n.index)}}return r}var S2,A2,$g=P(()=>{u();Dg();zt();S2=/([\[\]'"`])([^\[\]'"`])?/g,A2=/[^"'`\s<>\]]+/});function _2(r,e){let t=r.tailwindConfig.content.extract;return t[e]||t.DEFAULT||Mg[e]||Mg.DEFAULT(r)}function E2(r,e){let t=r.content.transform;return t[e]||t.DEFAULT||Ng[e]||Ng.DEFAULT}function O2(r,e,t,i){Li.has(e)||Li.set(e,new Lg.default({maxSize:25e3}));for(let n of r.split(` +`))if(n=n.trim(),!i.has(n))if(i.add(n),Li.get(e).has(n))for(let s of Li.get(e).get(n))t.add(s);else{let s=e(n).filter(o=>o!=="!*"),a=new Set(s);for(let o of a)t.add(o);Li.get(e).set(n,a)}}function T2(r,e){let t=e.offsets.sort(r),i={base:new Set,defaults:new Set,components:new Set,utilities:new Set,variants:new Set};for(let[n,s]of t)i[n.layer].add(s);return i}function Pl(r){return async e=>{let t={base:null,components:null,utilities:null,variants:null};if(e.walkAtRules(y=>{y.name==="tailwind"&&Object.keys(t).includes(y.params)&&(t[y.params]=y)}),Object.values(t).every(y=>y===null))return e;let i=new Set([...r.candidates??[],gt]),n=new Set;bt.DEBUG&&console.time("Reading changed files");let s=[];for(let y of r.changedContent){let w=E2(r.tailwindConfig,y.extension),k=_2(r,y.extension);s.push([y,{transformer:w,extractor:k}])}let a=500;for(let y=0;y{S=k?await be.promises.readFile(k,"utf8"):S,O2(E(S),T,i,n)}))}bt.DEBUG&&console.timeEnd("Reading changed files");let o=r.classCache.size;bt.DEBUG&&console.time("Generate rules"),bt.DEBUG&&console.time("Sorting candidates");let l=new Set([...i].sort((y,w)=>y===w?0:y{let w=y.raws.tailwind?.parentLayer;return w==="components"?t.components!==null:w==="utilities"?t.utilities!==null:!0});t.variants?(t.variants.before(Qt(b,t.variants.source,{layer:"variants"})),t.variants.remove()):b.length>0&&e.append(Qt(b,e.source,{layer:"variants"})),e.source.end=e.source.end??e.source.start;let v=b.some(y=>y.raws.tailwind?.parentLayer==="utilities");t.utilities&&p.size===0&&!v&&G.warn("content-problems",["No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.","https://tailwindcss.com/docs/content-configuration"]),bt.DEBUG&&(console.log("Potential classes: ",i.size),console.log("Active contexts: ",es.size)),r.changedContent=[],e.walkAtRules("layer",y=>{Object.keys(t).includes(y.params)&&y.remove()})}}var Lg,bt,Mg,Ng,Li,Bg=P(()=>{u();ft();Lg=pe(Fs());It();os();Be();Rg();$g();bt=Ze,Mg={DEFAULT:qg},Ng={DEFAULT:r=>r,svelte:r=>r.replace(/(?:^|\s)class:/g," ")};Li=new WeakMap});function xs(r){let e=new Map;ee.root({nodes:[r.clone()]}).walkRules(s=>{(0,vs.default)(a=>{a.walkClasses(o=>{let l=o.parent.toString(),c=e.get(l);c||e.set(l,c=new Set),c.add(o.value)})}).processSync(s.selector)});let i=Array.from(e.values(),s=>Array.from(s)),n=i.flat();return Object.assign(n,{groups:i})}function Il(r){return R2.astSync(r)}function Fg(r,e){let t=new Set;for(let i of r)t.add(i.split(e).pop());return Array.from(t)}function jg(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function*zg(r){for(yield r;r.parent;)yield r.parent,r=r.parent}function P2(r,e={}){let t=r.nodes;r.nodes=[];let i=r.clone(e);return r.nodes=t,i}function I2(r){for(let e of zg(r))if(r!==e){if(e.type==="root")break;r=P2(e,{nodes:[r]})}return r}function D2(r,e){let t=new Map;return r.walkRules(i=>{for(let a of zg(i))if(a.raws.tailwind?.layer!==void 0)return;let n=I2(i),s=e.offsets.create("user");for(let a of xs(i)){let o=t.get(a)||[];t.set(a,o),o.push([{layer:"user",sort:s,important:!1},n])}}),t}function q2(r,e){for(let t of r){if(e.notClassCache.has(t)||e.applyClassCache.has(t))continue;if(e.classCache.has(t)){e.applyClassCache.set(t,e.classCache.get(t).map(([n,s])=>[n,s.clone()]));continue}let i=Array.from(Yo(t,e));if(i.length===0){e.notClassCache.add(t);continue}e.applyClassCache.set(t,i)}return e.applyClassCache}function $2(r){let e=null;return{get:t=>(e=e||r(),e.get(t)),has:t=>(e=e||r(),e.has(t))}}function L2(r){return{get:e=>r.flatMap(t=>t.get(e)||[]),has:e=>r.some(t=>t.has(e))}}function Ug(r){let e=r.split(/[\s\t\n]+/g);return e[e.length-1]==="!important"?[e.slice(0,-1),!0]:[e,!1]}function Vg(r,e,t){let i=new Set,n=[];if(r.walkAtRules("apply",l=>{let[c]=Ug(l.params);for(let f of c)i.add(f);n.push(l)}),n.length===0)return;let s=L2([t,q2(i,e)]);function a(l,c,f){let d=Il(l),p=Il(c),b=Il(`.${Te(f)}`).nodes[0].nodes[0];return d.each(v=>{let y=new Set;p.each(w=>{let k=!1;w=w.clone(),w.walkClasses(S=>{S.value===b.value&&(k||(S.replaceWith(...v.nodes.map(E=>E.clone())),y.add(w),k=!0))})});for(let w of y){let k=[[]];for(let S of w.nodes)S.type==="combinator"?(k.push(S),k.push([])):k[k.length-1].push(S);w.nodes=[];for(let S of k)Array.isArray(S)&&S.sort((E,T)=>E.type==="tag"&&T.type==="class"?-1:E.type==="class"&&T.type==="tag"?1:E.type==="class"&&T.type==="pseudo"&&T.value.startsWith("::")?-1:E.type==="pseudo"&&E.value.startsWith("::")&&T.type==="class"?1:0),w.nodes=w.nodes.concat(S)}v.replaceWith(...y)}),d.toString()}let o=new Map;for(let l of n){let[c]=o.get(l.parent)||[[],l.source];o.set(l.parent,[c,l.source]);let[f,d]=Ug(l.params);if(l.parent.type==="atrule"){if(l.parent.name==="screen"){let p=l.parent.params;throw l.error(`@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${f.map(h=>`${p}:${h}`).join(" ")} instead.`)}throw l.error(`@apply is not supported within nested at-rules like @${l.parent.name}. You can fix this by un-nesting @${l.parent.name}.`)}for(let p of f){if([jg(e,"group"),jg(e,"peer")].includes(p))throw l.error(`@apply should not be used with the '${p}' utility`);if(!s.has(p))throw l.error(`The \`${p}\` class does not exist. If \`${p}\` is a custom class, make sure it is defined within a \`@layer\` directive.`);let h=s.get(p);for(let[,b]of h)b.type!=="atrule"&&b.walkRules(()=>{throw l.error([`The \`${p}\` class cannot be used with \`@apply\` because \`@apply\` does not currently support nested CSS.`,"Rewrite the selector without nesting or configure the `tailwindcss/nesting` plugin:","https://tailwindcss.com/docs/using-with-preprocessors#nesting"].join(` +`))});c.push([p,d,h])}}for(let[l,[c,f]]of o){let d=[];for(let[h,b,v]of c){let y=[h,...Fg([h],e.tailwindConfig.separator)];for(let[w,k]of v){let S=xs(l),E=xs(k);if(E=E.groups.filter(R=>R.some(F=>y.includes(F))).flat(),E=E.concat(Fg(E,e.tailwindConfig.separator)),S.some(R=>E.includes(R)))throw k.error(`You cannot \`@apply\` the \`${h}\` utility here because it creates a circular dependency.`);let B=ee.root({nodes:[k.clone()]});B.walk(R=>{R.source=f}),(k.type!=="atrule"||k.type==="atrule"&&k.name!=="keyframes")&&B.walkRules(R=>{if(!xs(R).some(U=>U===h)){R.remove();return}let F=typeof e.tailwindConfig.important=="string"?e.tailwindConfig.important:null,_=l.raws.tailwind!==void 0&&F&&l.selector.indexOf(F)===0?l.selector.slice(F.length):l.selector;_===""&&(_=l.selector),R.selector=a(_,R.selector,h),F&&_!==l.selector&&(R.selector=is(R.selector,F)),R.walkDecls(U=>{U.important=w.important||b});let Q=(0,vs.default)().astSync(R.selector);Q.each(U=>pr(U)),R.selector=Q.toString()}),!!B.nodes[0]&&d.push([w.sort,B.nodes[0]])}}let p=e.offsets.sort(d).map(h=>h[1]);l.after(p)}for(let l of n)l.parent.nodes.length>1?l.remove():l.parent.remove();Vg(r,e,t)}function Dl(r){return e=>{let t=$2(()=>D2(e,r));Vg(e,r,t)}}var vs,R2,Hg=P(()=>{u();Ot();vs=pe(it());os();fr();Wo();ts();R2=(0,vs.default)()});var Wg=x((nq,ks)=>{u();(function(){"use strict";function r(i,n,s){if(!i)return null;r.caseSensitive||(i=i.toLowerCase());var a=r.threshold===null?null:r.threshold*i.length,o=r.thresholdAbsolute,l;a!==null&&o!==null?l=Math.min(a,o):a!==null?l=a:o!==null?l=o:l=null;var c,f,d,p,h,b=n.length;for(h=0;hs)return s+1;var l=[],c,f,d,p,h;for(c=0;c<=o;c++)l[c]=[c];for(f=0;f<=a;f++)l[0][f]=f;for(c=1;c<=o;c++){for(d=e,p=1,c>s&&(p=c-s),h=o+1,h>s+c&&(h=s+c),f=1;f<=a;f++)fh?l[c][f]=s+1:n.charAt(c-1)===i.charAt(f-1)?l[c][f]=l[c-1][f-1]:l[c][f]=Math.min(l[c-1][f-1]+1,Math.min(l[c][f-1]+1,l[c-1][f]+1)),l[c][f]s)return s+1}return l[o][a]}})()});var Qg=x((sq,Gg)=>{u();var ql="(".charCodeAt(0),$l=")".charCodeAt(0),Ss="'".charCodeAt(0),Ll='"'.charCodeAt(0),Ml="\\".charCodeAt(0),yr="/".charCodeAt(0),Nl=",".charCodeAt(0),Bl=":".charCodeAt(0),As="*".charCodeAt(0),M2="u".charCodeAt(0),N2="U".charCodeAt(0),B2="+".charCodeAt(0),F2=/^[a-f0-9?-]+$/i;Gg.exports=function(r){for(var e=[],t=r,i,n,s,a,o,l,c,f,d=0,p=t.charCodeAt(d),h=t.length,b=[{nodes:e}],v=0,y,w="",k="",S="";d{u();Yg.exports=function r(e,t,i){var n,s,a,o;for(n=0,s=e.length;n{u();function Xg(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Zg(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Zg(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Xg(r[i],e)+t;return t}return Xg(r,e)}Jg.exports=Zg});var ry=x((lq,ty)=>{u();var Cs="-".charCodeAt(0),_s="+".charCodeAt(0),Fl=".".charCodeAt(0),j2="e".charCodeAt(0),z2="E".charCodeAt(0);function U2(r){var e=r.charCodeAt(0),t;if(e===_s||e===Cs){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===Fl&&i>=48&&i<=57}return e===Fl?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}ty.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!U2(r))return!1;for(i=r.charCodeAt(e),(i===_s||i===Cs)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===Fl&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),s=r.charCodeAt(e+2),(i===j2||i===z2)&&(n>=48&&n<=57||(n===_s||n===Cs)&&s>=48&&s<=57))for(e+=n===_s||n===Cs?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var ay=x((uq,sy)=>{u();var V2=Qg(),iy=Kg(),ny=ey();function $t(r){return this instanceof $t?(this.nodes=V2(r),this):new $t(r)}$t.prototype.toString=function(){return Array.isArray(this.nodes)?ny(this.nodes):""};$t.prototype.walk=function(r,e){return iy(this.nodes,r,e),this};$t.unit=ry();$t.walk=iy;$t.stringify=ny;sy.exports=$t});function zl(r){return typeof r=="object"&&r!==null}function H2(r,e){let t=kt(e);do if(t.pop(),(0,Mi.default)(r,t)!==void 0)break;while(t.length);return t.length?t:void 0}function br(r){return typeof r=="string"?r:r.reduce((e,t,i)=>t.includes(".")?`${e}[${t}]`:i===0?t:`${e}.${t}`,"")}function ly(r){return r.map(e=>`'${e}'`).join(", ")}function uy(r){return ly(Object.keys(r))}function Ul(r,e,t,i={}){let n=Array.isArray(e)?br(e):e.replace(/^['"]+|['"]+$/g,""),s=Array.isArray(e)?e:kt(n),a=(0,Mi.default)(r.theme,s,t);if(a===void 0){let l=`'${n}' does not exist in your theme config.`,c=s.slice(0,-1),f=(0,Mi.default)(r.theme,c);if(zl(f)){let d=Object.keys(f).filter(h=>Ul(r,[...c,h]).isValid),p=(0,oy.default)(s[s.length-1],d);p?l+=` Did you mean '${br([...c,p])}'?`:d.length>0&&(l+=` '${br(c)}' has the following valid keys: ${ly(d)}`)}else{let d=H2(r.theme,n);if(d){let p=(0,Mi.default)(r.theme,d);zl(p)?l+=` '${br(d)}' has the following keys: ${uy(p)}`:l+=` '${br(d)}' is not an object.`}else l+=` Your theme has the following top-level keys: ${uy(r.theme)}`}return{isValid:!1,error:l}}if(!(typeof a=="string"||typeof a=="number"||typeof a=="function"||a instanceof String||a instanceof Number||Array.isArray(a))){let l=`'${n}' was found but does not resolve to a string.`;if(zl(a)){let c=Object.keys(a).filter(f=>Ul(r,[...s,f]).isValid);c.length&&(l+=` Did you mean something like '${br([...s,c[0]])}'?`)}return{isValid:!1,error:l}}let[o]=s;return{isValid:!0,value:mt(o)(a,i)}}function W2(r,e,t){e=e.map(n=>fy(r,n,t));let i=[""];for(let n of e)n.type==="div"&&n.value===","?i.push(""):i[i.length-1]+=jl.default.stringify(n);return i}function fy(r,e,t){if(e.type==="function"&&t[e.value]!==void 0){let i=W2(r,e.nodes,t);e.type="word",e.value=t[e.value](r,...i)}return e}function G2(r,e,t){return Object.keys(t).some(n=>e.includes(`${n}(`))?(0,jl.default)(e).walk(n=>{fy(r,n,t)}).toString():e}function*Y2(r){r=r.replace(/^['"]+|['"]+$/g,"");let e=r.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/),t;yield[r,void 0],e&&(r=e[1],t=e[2],yield[r,t])}function K2(r,e,t){let i=Array.from(Y2(e)).map(([n,s])=>Object.assign(Ul(r,n,t,{opacityValue:s}),{resolvedPath:n,alpha:s}));return i.find(n=>n.isValid)??i[0]}function cy(r){let e=r.tailwindConfig,t={theme:(i,n,...s)=>{let{isValid:a,value:o,error:l,alpha:c}=K2(e,n,s.length?s:void 0);if(!a){let p=i.parent,h=p?.raws.tailwind?.candidate;if(p&&h!==void 0){r.markInvalidUtilityNode(p),p.remove(),G.warn("invalid-theme-key-in-class",[`The utility \`${h}\` contains an invalid theme value and was not generated.`]);return}throw i.error(l)}let f=Xt(o),d=f!==void 0&&typeof f=="function";return(c!==void 0||d)&&(c===void 0&&(c=1),o=Je(f,c,f)),o},screen:(i,n)=>{n=n.replace(/^['"]+/g,"").replace(/['"]+$/g,"");let a=Rt(e.theme.screens).find(({name:o})=>o===n);if(!a)throw i.error(`The '${n}' screen does not exist in your theme.`);return Tt(a)}};return i=>{i.walk(n=>{let s=Q2[n.type];s!==void 0&&(n[s]=G2(n,n[s],t))})}}var Mi,oy,jl,Q2,py=P(()=>{u();Mi=pe(Ra()),oy=pe(Wg());Ci();jl=pe(ay());Zn();Yn();Yi();Lr();Fr();Be();Q2={atrule:"params",decl:"value"}});function dy({tailwindConfig:{theme:r}}){return function(e){e.walkAtRules("screen",t=>{let i=t.params,s=Rt(r.screens).find(({name:a})=>a===i);if(!s)throw t.error(`No \`${i}\` screen found.`);t.name="media",t.params=Tt(s)})}}var hy=P(()=>{u();Zn();Yn()});function X2(r){let e=r.filter(o=>o.type!=="pseudo"||o.nodes.length>0?!0:o.value.startsWith("::")||[":before",":after",":first-line",":first-letter"].includes(o.value)).reverse(),t=new Set(["tag","class","id","attribute"]),i=e.findIndex(o=>t.has(o.type));if(i===-1)return e.reverse().join("").trim();let n=e[i],s=my[n.type]?my[n.type](n):n;e=e.slice(0,i);let a=e.findIndex(o=>o.type==="combinator"&&o.value===">");return a!==-1&&(e.splice(0,a),e.unshift(Es.default.universal())),[s,...e.reverse()].join("").trim()}function J2(r){return Vl.has(r)||Vl.set(r,Z2.transformSync(r)),Vl.get(r)}function Hl({tailwindConfig:r}){return e=>{let t=new Map,i=new Set;if(e.walkAtRules("defaults",n=>{if(n.nodes&&n.nodes.length>0){i.add(n);return}let s=n.params;t.has(s)||t.set(s,new Set),t.get(s).add(n.parent),n.remove()}),we(r,"optimizeUniversalDefaults"))for(let n of i){let s=new Map,a=t.get(n.params)??[];for(let o of a)for(let l of J2(o.selector)){let c=l.includes(":-")||l.includes("::-")||l.includes(":has")?l:"__DEFAULT__",f=s.get(c)??new Set;s.set(c,f),f.add(l)}if(s.size===0){n.remove();continue}for(let[,o]of s){let l=ee.rule({source:n.source});l.selectors=[...o],l.append(n.nodes.map(c=>c.clone())),n.before(l)}n.remove()}else if(i.size){let n=ee.rule({selectors:["*","::before","::after"]});for(let a of i)n.append(a.nodes),n.parent||a.before(n),n.source||(n.source=a.source),a.remove();let s=n.clone({selectors:["::backdrop"]});n.after(s)}}}var Es,my,Z2,Vl,gy=P(()=>{u();Ot();Es=pe(it());ct();my={id(r){return Es.default.attribute({attribute:"id",operator:"=",value:r.value,quoteMark:'"'})}};Z2=(0,Es.default)(r=>r.map(e=>{let t=e.split(i=>i.type==="combinator"&&i.value===" ").pop();return X2(t)})),Vl=new Map});function Wl(){function r(e){let t=null;e.each(i=>{if(!eO.has(i.type)){t=null;return}if(t===null){t=i;return}let n=yy[i.type];i.type==="atrule"&&i.name==="font-face"?t=i:n.every(s=>(i[s]??"").replace(/\s+/g," ")===(t[s]??"").replace(/\s+/g," "))?(i.nodes&&t.append(i.nodes),i.remove()):t=i}),e.each(i=>{i.type==="atrule"&&r(i)})}return e=>{r(e)}}var yy,eO,by=P(()=>{u();yy={atrule:["name","params"],rule:["selector"]},eO=new Set(Object.keys(yy))});function Gl(){return r=>{r.walkRules(e=>{let t=new Map,i=new Set([]),n=new Map;e.walkDecls(s=>{if(s.parent===e){if(t.has(s.prop)){if(t.get(s.prop).value===s.value){i.add(t.get(s.prop)),t.set(s.prop,s);return}n.has(s.prop)||n.set(s.prop,new Set),n.get(s.prop).add(t.get(s.prop)),n.get(s.prop).add(s)}t.set(s.prop,s)}});for(let s of i)s.remove();for(let s of n.values()){let a=new Map;for(let o of s){let l=rO(o.value);l!==null&&(a.has(l)||a.set(l,new Set),a.get(l).add(o))}for(let o of a.values()){let l=Array.from(o).slice(0,-1);for(let c of l)c.remove()}}})}}function rO(r){let e=/^-?\d*.?\d+([\w%]+)?$/g.exec(r);return e?e[1]??tO:null}var tO,wy=P(()=>{u();tO=Symbol("unitless-number")});function iO(r){if(!r.walkAtRules)return;let e=new Set;if(r.walkAtRules("apply",t=>{e.add(t.parent)}),e.size!==0)for(let t of e){let i=[],n=[];for(let s of t.nodes)s.type==="atrule"&&s.name==="apply"?(n.length>0&&(i.push(n),n=[]),i.push([s])):n.push(s);if(n.length>0&&i.push(n),i.length!==1){for(let s of[...i].reverse()){let a=t.clone({nodes:[]});a.append(s),t.after(a)}t.remove()}}}function Os(){return r=>{iO(r)}}var vy=P(()=>{u()});function Ts(r){return async function(e,t){let{tailwindDirectives:i,applyDirectives:n}=Ol(e);Os()(e,t);let s=r({tailwindDirectives:i,applyDirectives:n,registerDependency(a){t.messages.push({plugin:"tailwindcss",parent:t.opts.from,...a})},createContext(a,o){return il(a,o,e)}})(e,t);if(s.tailwindConfig.separator==="-")throw new Error("The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead.");Rf(s.tailwindConfig),await Pl(s)(e,t),Os()(e,t),Dl(s)(e,t),cy(s)(e,t),dy(s)(e,t),Hl(s)(e,t),Wl(s)(e,t),Gl(s)(e,t)}}var xy=P(()=>{u();Og();Bg();Hg();py();hy();gy();by();wy();vy();Oi();ct()});function ky(r,e){let t=null,i=null;return r.walkAtRules("config",n=>{if(i=n.source?.input.file??e.opts.from??null,i===null)throw n.error("The `@config` directive cannot be used without setting `from` in your PostCSS config.");if(t)throw n.error("Only one `@config` directive is allowed per file.");let s=n.params.match(/(['"])(.*?)\1/);if(!s)throw n.error("A path is required when using the `@config` directive.");let a=s[2];if(me.isAbsolute(a))throw n.error("The `@config` directive cannot be used with an absolute path.");if(t=me.resolve(me.dirname(i),a),!be.existsSync(t))throw n.error(`The config file at "${a}" does not exist. Make sure the path is correct and the file exists.`);n.remove()}),t||null}var Sy=P(()=>{u();ft();et()});var Ay=x((Wq,Ql)=>{u();Eg();xy();It();Sy();Ql.exports=function(e){return{postcssPlugin:"tailwindcss",plugins:[Ze.DEBUG&&function(t){return console.log(` +`),console.time("JIT TOTAL"),t},async function(t,i){e=ky(t,i)??e;let n=El(e);if(t.type==="document"){let s=t.nodes.filter(a=>a.type==="root");for(let a of s)a.type==="root"&&await Ts(n)(a,i);return}await Ts(n)(t,i)},Ze.DEBUG&&function(t){return console.timeEnd("JIT TOTAL"),console.log(` +`),t}].filter(Boolean)}};Ql.exports.postcss=!0});var _y=x((Gq,Cy)=>{u();Cy.exports=Ay()});var Yl=x((Qq,Ey)=>{u();Ey.exports=()=>["and_chr 114","and_uc 15.5","chrome 114","chrome 113","chrome 109","edge 114","firefox 114","ios_saf 16.5","ios_saf 16.4","ios_saf 16.3","ios_saf 16.1","opera 99","safari 16.5","samsung 21"]});var Rs={};Ge(Rs,{agents:()=>nO,feature:()=>sO});function sO(){return{status:"cr",title:"CSS Feature Queries",stats:{ie:{"6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","5.5":"n"},edge:{"12":"y","13":"y","14":"y","15":"y","16":"y","17":"y","18":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y"},firefox:{"2":"n","3":"n","4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y","3.5":"n","3.6":"n"},chrome:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"n","23":"n","24":"n","25":"n","26":"n","27":"n","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y"},safari:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"y","10":"y","11":"y","12":"y","13":"y","14":"y","15":"y","17":"y","9.1":"y","10.1":"y","11.1":"y","12.1":"y","13.1":"y","14.1":"y","15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y",TP:"y","3.1":"n","3.2":"n","5.1":"n","6.1":"n","7.1":"n"},opera:{"9":"n","11":"n","12":"n","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","60":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","12.1":"y","9.5-9.6":"n","10.0-10.1":"n","10.5":"n","10.6":"n","11.1":"n","11.5":"n","11.6":"n"},ios_saf:{"8":"n","17":"y","9.0-9.2":"y","9.3":"y","10.0-10.2":"y","10.3":"y","11.0-11.2":"y","11.3-11.4":"y","12.0-12.1":"y","12.2-12.5":"y","13.0-13.1":"y","13.2":"y","13.3":"y","13.4-13.7":"y","14.0-14.4":"y","14.5-14.8":"y","15.0-15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y","3.2":"n","4.0-4.1":"n","4.2-4.3":"n","5.0-5.1":"n","6.0-6.1":"n","7.0-7.1":"n","8.1-8.4":"n"},op_mini:{all:"y"},android:{"3":"n","4":"n","114":"y","4.4":"y","4.4.3-4.4.4":"y","2.1":"n","2.2":"n","2.3":"n","4.1":"n","4.2-4.3":"n"},bb:{"7":"n","10":"n"},op_mob:{"10":"n","11":"n","12":"n","73":"y","11.1":"n","11.5":"n","12.1":"n"},and_chr:{"114":"y"},and_ff:{"115":"y"},ie_mob:{"10":"n","11":"n"},and_uc:{"15.5":"y"},samsung:{"4":"y","20":"y","21":"y","5.0-5.4":"y","6.2-6.4":"y","7.2-7.4":"y","8.2":"y","9.2":"y","10.1":"y","11.1-11.2":"y","12.0":"y","13.0":"y","14.0":"y","15.0":"y","16.0":"y","17.0":"y","18.0":"y","19.0":"y"},and_qq:{"13.1":"y"},baidu:{"13.18":"y"},kaios:{"2.5":"y","3.0-3.1":"y"}}}}var nO,Ps=P(()=>{u();nO={ie:{prefix:"ms"},edge:{prefix:"webkit",prefix_exceptions:{"12":"ms","13":"ms","14":"ms","15":"ms","16":"ms","17":"ms","18":"ms"}},firefox:{prefix:"moz"},chrome:{prefix:"webkit"},safari:{prefix:"webkit"},opera:{prefix:"webkit",prefix_exceptions:{"9":"o","11":"o","12":"o","9.5-9.6":"o","10.0-10.1":"o","10.5":"o","10.6":"o","11.1":"o","11.5":"o","11.6":"o","12.1":"o"}},ios_saf:{prefix:"webkit"},op_mini:{prefix:"o"},android:{prefix:"webkit"},bb:{prefix:"webkit"},op_mob:{prefix:"o",prefix_exceptions:{"73":"webkit"}},and_chr:{prefix:"webkit"},and_ff:{prefix:"moz"},ie_mob:{prefix:"ms"},and_uc:{prefix:"webkit",prefix_exceptions:{"15.5":"webkit"}},samsung:{prefix:"webkit"},and_qq:{prefix:"webkit"},baidu:{prefix:"webkit"},kaios:{prefix:"moz"}}});var Oy=x(()=>{u()});var _e=x((Xq,Lt)=>{u();var{list:Kl}=$e();Lt.exports.error=function(r){let e=new Error(r);throw e.autoprefixer=!0,e};Lt.exports.uniq=function(r){return[...new Set(r)]};Lt.exports.removeNote=function(r){return r.includes(" ")?r.split(" ")[0]:r};Lt.exports.escapeRegexp=function(r){return r.replace(/[$()*+-.?[\\\]^{|}]/g,"\\$&")};Lt.exports.regexp=function(r,e=!0){return e&&(r=this.escapeRegexp(r)),new RegExp(`(^|[\\s,(])(${r}($|[\\s(,]))`,"gi")};Lt.exports.editList=function(r,e){let t=Kl.comma(r),i=e(t,[]);if(t===i)return r;let n=r.match(/,\s*/);return n=n?n[0]:", ",i.join(n)};Lt.exports.splitSelector=function(r){return Kl.comma(r).map(e=>Kl.space(e).map(t=>t.split(/(?=\.|#)/g)))}});var Mt=x((Zq,Py)=>{u();var aO=Yl(),Ty=(Ps(),Rs).agents,oO=_e(),Ry=class{static prefixes(){if(this.prefixesCache)return this.prefixesCache;this.prefixesCache=[];for(let e in Ty)this.prefixesCache.push(`-${Ty[e].prefix}-`);return this.prefixesCache=oO.uniq(this.prefixesCache).sort((e,t)=>t.length-e.length),this.prefixesCache}static withPrefix(e){return this.prefixesRegexp||(this.prefixesRegexp=new RegExp(this.prefixes().join("|"))),this.prefixesRegexp.test(e)}constructor(e,t,i,n){this.data=e,this.options=i||{},this.browserslistOpts=n||{},this.selected=this.parse(t)}parse(e){let t={};for(let i in this.browserslistOpts)t[i]=this.browserslistOpts[i];return t.path=this.options.from,aO(e,t)}prefix(e){let[t,i]=e.split(" "),n=this.data[t],s=n.prefix_exceptions&&n.prefix_exceptions[i];return s||(s=n.prefix),`-${s}-`}isSelected(e){return this.selected.includes(e)}};Py.exports=Ry});var Ni=x((Jq,Iy)=>{u();Iy.exports={prefix(r){let e=r.match(/^(-\w+-)/);return e?e[0]:""},unprefixed(r){return r.replace(/^-\w+-/,"")}}});var wr=x((e$,qy)=>{u();var lO=Mt(),Dy=Ni(),uO=_e();function Xl(r,e){let t=new r.constructor;for(let i of Object.keys(r||{})){let n=r[i];i==="parent"&&typeof n=="object"?e&&(t[i]=e):i==="source"||i===null?t[i]=n:Array.isArray(n)?t[i]=n.map(s=>Xl(s,t)):i!=="_autoprefixerPrefix"&&i!=="_autoprefixerValues"&&i!=="proxyCache"&&(typeof n=="object"&&n!==null&&(n=Xl(n,t)),t[i]=n)}return t}var Is=class{static hack(e){return this.hacks||(this.hacks={}),e.names.map(t=>(this.hacks[t]=e,this.hacks[t]))}static load(e,t,i){let n=this.hacks&&this.hacks[e];return n?new n(e,t,i):new this(e,t,i)}static clone(e,t){let i=Xl(e);for(let n in t)i[n]=t[n];return i}constructor(e,t,i){this.prefixes=t,this.name=e,this.all=i}parentPrefix(e){let t;return typeof e._autoprefixerPrefix!="undefined"?t=e._autoprefixerPrefix:e.type==="decl"&&e.prop[0]==="-"?t=Dy.prefix(e.prop):e.type==="root"?t=!1:e.type==="rule"&&e.selector.includes(":-")&&/:(-\w+-)/.test(e.selector)?t=e.selector.match(/:(-\w+-)/)[1]:e.type==="atrule"&&e.name[0]==="-"?t=Dy.prefix(e.name):t=this.parentPrefix(e.parent),lO.prefixes().includes(t)||(t=!1),e._autoprefixerPrefix=t,e._autoprefixerPrefix}process(e,t){if(!this.check(e))return;let i=this.parentPrefix(e),n=this.prefixes.filter(a=>!i||i===uO.removeNote(a)),s=[];for(let a of n)this.add(e,a,s.concat([a]),t)&&s.push(a);return s}clone(e,t){return Is.clone(e,t)}};qy.exports=Is});var j=x((t$,My)=>{u();var fO=wr(),cO=Mt(),$y=_e(),Ly=class extends fO{check(){return!0}prefixed(e,t){return t+e}normalize(e){return e}otherPrefixes(e,t){for(let i of cO.prefixes())if(i!==t&&e.includes(i))return!0;return!1}set(e,t){return e.prop=this.prefixed(e.prop,t),e}needCascade(e){return e._autoprefixerCascade||(e._autoprefixerCascade=this.all.options.cascade!==!1&&e.raw("before").includes(` +`)),e._autoprefixerCascade}maxPrefixed(e,t){if(t._autoprefixerMax)return t._autoprefixerMax;let i=0;for(let n of e)n=$y.removeNote(n),n.length>i&&(i=n.length);return t._autoprefixerMax=i,t._autoprefixerMax}calcBefore(e,t,i=""){let s=this.maxPrefixed(e,t)-$y.removeNote(i).length,a=t.raw("before");return s>0&&(a+=Array(s).fill(" ").join("")),a}restoreBefore(e){let t=e.raw("before").split(` +`),i=t[t.length-1];this.all.group(e).up(n=>{let s=n.raw("before").split(` +`),a=s[s.length-1];a.lengtha.prop===n.prop&&a.value===n.value)))return this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,n)}isAlready(e,t){let i=this.all.group(e).up(n=>n.prop===t);return i||(i=this.all.group(e).down(n=>n.prop===t)),i}add(e,t,i,n){let s=this.prefixed(e.prop,t);if(!(this.isAlready(e,s)||this.otherPrefixes(e.value,t)))return this.insert(e,t,i,n)}process(e,t){if(!this.needCascade(e)){super.process(e,t);return}let i=super.process(e,t);!i||!i.length||(this.restoreBefore(e),e.raws.before=this.calcBefore(i,e))}old(e,t){return[this.prefixed(e,t)]}};My.exports=Ly});var By=x((r$,Ny)=>{u();Ny.exports=function r(e){return{mul:t=>new r(e*t),div:t=>new r(e/t),simplify:()=>new r(e),toString:()=>e.toString()}}});var zy=x((i$,jy)=>{u();var pO=By(),dO=wr(),Zl=_e(),hO=/(min|max)-resolution\s*:\s*\d*\.?\d+(dppx|dpcm|dpi|x)/gi,mO=/(min|max)-resolution(\s*:\s*)(\d*\.?\d+)(dppx|dpcm|dpi|x)/i,Fy=class extends dO{prefixName(e,t){return e==="-moz-"?t+"--moz-device-pixel-ratio":e+t+"-device-pixel-ratio"}prefixQuery(e,t,i,n,s){return n=new pO(n),s==="dpi"?n=n.div(96):s==="dpcm"&&(n=n.mul(2.54).div(96)),n=n.simplify(),e==="-o-"&&(n=n.n+"/"+n.d),this.prefixName(e,t)+i+n}clean(e){if(!this.bad){this.bad=[];for(let t of this.prefixes)this.bad.push(this.prefixName(t,"min")),this.bad.push(this.prefixName(t,"max"))}e.params=Zl.editList(e.params,t=>t.filter(i=>this.bad.every(n=>!i.includes(n))))}process(e){let t=this.parentPrefix(e),i=t?[t]:this.prefixes;e.params=Zl.editList(e.params,(n,s)=>{for(let a of n){if(!a.includes("min-resolution")&&!a.includes("max-resolution")){s.push(a);continue}for(let o of i){let l=a.replace(hO,c=>{let f=c.match(mO);return this.prefixQuery(o,f[1],f[2],f[3],f[4])});s.push(l)}s.push(a)}return Zl.uniq(s)})}};jy.exports=Fy});var Vy=x((n$,Uy)=>{u();var Jl="(".charCodeAt(0),eu=")".charCodeAt(0),Ds="'".charCodeAt(0),tu='"'.charCodeAt(0),ru="\\".charCodeAt(0),vr="/".charCodeAt(0),iu=",".charCodeAt(0),nu=":".charCodeAt(0),qs="*".charCodeAt(0),gO="u".charCodeAt(0),yO="U".charCodeAt(0),bO="+".charCodeAt(0),wO=/^[a-f0-9?-]+$/i;Uy.exports=function(r){for(var e=[],t=r,i,n,s,a,o,l,c,f,d=0,p=t.charCodeAt(d),h=t.length,b=[{nodes:e}],v=0,y,w="",k="",S="";d{u();Hy.exports=function r(e,t,i){var n,s,a,o;for(n=0,s=e.length;n{u();function Gy(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Qy(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Qy(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Gy(r[i],e)+t;return t}return Gy(r,e)}Yy.exports=Qy});var Zy=x((o$,Xy)=>{u();var $s="-".charCodeAt(0),Ls="+".charCodeAt(0),su=".".charCodeAt(0),vO="e".charCodeAt(0),xO="E".charCodeAt(0);function kO(r){var e=r.charCodeAt(0),t;if(e===Ls||e===$s){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===su&&i>=48&&i<=57}return e===su?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Xy.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!kO(r))return!1;for(i=r.charCodeAt(e),(i===Ls||i===$s)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===su&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),s=r.charCodeAt(e+2),(i===vO||i===xO)&&(n>=48&&n<=57||(n===Ls||n===$s)&&s>=48&&s<=57))for(e+=n===Ls||n===$s?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var Ms=x((l$,tb)=>{u();var SO=Vy(),Jy=Wy(),eb=Ky();function Nt(r){return this instanceof Nt?(this.nodes=SO(r),this):new Nt(r)}Nt.prototype.toString=function(){return Array.isArray(this.nodes)?eb(this.nodes):""};Nt.prototype.walk=function(r,e){return Jy(this.nodes,r,e),this};Nt.unit=Zy();Nt.walk=Jy;Nt.stringify=eb;tb.exports=Nt});var ab=x((u$,sb)=>{u();var{list:AO}=$e(),rb=Ms(),CO=Mt(),ib=Ni(),nb=class{constructor(e){this.props=["transition","transition-property"],this.prefixes=e}add(e,t){let i,n,s=this.prefixes.add[e.prop],a=this.ruleVendorPrefixes(e),o=a||s&&s.prefixes||[],l=this.parse(e.value),c=l.map(h=>this.findProp(h)),f=[];if(c.some(h=>h[0]==="-"))return;for(let h of l){if(n=this.findProp(h),n[0]==="-")continue;let b=this.prefixes.add[n];if(!(!b||!b.prefixes))for(i of b.prefixes){if(a&&!a.some(y=>i.includes(y)))continue;let v=this.prefixes.prefixed(n,i);v!=="-ms-transform"&&!c.includes(v)&&(this.disabled(n,i)||f.push(this.clone(n,v,h)))}}l=l.concat(f);let d=this.stringify(l),p=this.stringify(this.cleanFromUnprefixed(l,"-webkit-"));if(o.includes("-webkit-")&&this.cloneBefore(e,`-webkit-${e.prop}`,p),this.cloneBefore(e,e.prop,p),o.includes("-o-")){let h=this.stringify(this.cleanFromUnprefixed(l,"-o-"));this.cloneBefore(e,`-o-${e.prop}`,h)}for(i of o)if(i!=="-webkit-"&&i!=="-o-"){let h=this.stringify(this.cleanOtherPrefixes(l,i));this.cloneBefore(e,i+e.prop,h)}d!==e.value&&!this.already(e,e.prop,d)&&(this.checkForWarning(t,e),e.cloneBefore(),e.value=d)}findProp(e){let t=e[0].value;if(/^\d/.test(t)){for(let[i,n]of e.entries())if(i!==0&&n.type==="word")return n.value}return t}already(e,t,i){return e.parent.some(n=>n.prop===t&&n.value===i)}cloneBefore(e,t,i){this.already(e,t,i)||e.cloneBefore({prop:t,value:i})}checkForWarning(e,t){if(t.prop!=="transition-property")return;let i=!1,n=!1;t.parent.each(s=>{if(s.type!=="decl"||s.prop.indexOf("transition-")!==0)return;let a=AO.comma(s.value);if(s.prop==="transition-property"){a.forEach(o=>{let l=this.prefixes.add[o];l&&l.prefixes&&l.prefixes.length>0&&(i=!0)});return}return n=n||a.length>1,!1}),i&&n&&t.warn(e,"Replace transition-property to transition, because Autoprefixer could not support any cases of transition-property and other transition-*")}remove(e){let t=this.parse(e.value);t=t.filter(a=>{let o=this.prefixes.remove[this.findProp(a)];return!o||!o.remove});let i=this.stringify(t);if(e.value===i)return;if(t.length===0){e.remove();return}let n=e.parent.some(a=>a.prop===e.prop&&a.value===i),s=e.parent.some(a=>a!==e&&a.prop===e.prop&&a.value.length>i.length);if(n||s){e.remove();return}e.value=i}parse(e){let t=rb(e),i=[],n=[];for(let s of t.nodes)n.push(s),s.type==="div"&&s.value===","&&(i.push(n),n=[]);return i.push(n),i.filter(s=>s.length>0)}stringify(e){if(e.length===0)return"";let t=[];for(let i of e)i[i.length-1].type!=="div"&&i.push(this.div(e)),t=t.concat(i);return t[0].type==="div"&&(t=t.slice(1)),t[t.length-1].type==="div"&&(t=t.slice(0,-2+1||void 0)),rb.stringify({nodes:t})}clone(e,t,i){let n=[],s=!1;for(let a of i)!s&&a.type==="word"&&a.value===e?(n.push({type:"word",value:t}),s=!0):n.push(a);return n}div(e){for(let t of e)for(let i of t)if(i.type==="div"&&i.value===",")return i;return{type:"div",value:",",after:" "}}cleanOtherPrefixes(e,t){return e.filter(i=>{let n=ib.prefix(this.findProp(i));return n===""||n===t})}cleanFromUnprefixed(e,t){let i=e.map(s=>this.findProp(s)).filter(s=>s.slice(0,t.length)===t).map(s=>this.prefixes.unprefixed(s)),n=[];for(let s of e){let a=this.findProp(s),o=ib.prefix(a);!i.includes(a)&&(o===t||o==="")&&n.push(s)}return n}disabled(e,t){let i=["order","justify-content","align-self","align-content"];if(e.includes("flex")||i.includes(e)){if(this.prefixes.options.flexbox===!1)return!0;if(this.prefixes.options.flexbox==="no-2009")return t.includes("2009")}}ruleVendorPrefixes(e){let{parent:t}=e;if(t.type!=="rule")return!1;if(!t.selector.includes(":-"))return!1;let i=CO.prefixes().filter(n=>t.selector.includes(":"+n));return i.length>0?i:!1}};sb.exports=nb});var xr=x((f$,lb)=>{u();var _O=_e(),ob=class{constructor(e,t,i,n){this.unprefixed=e,this.prefixed=t,this.string=i||t,this.regexp=n||_O.regexp(t)}check(e){return e.includes(this.string)?!!e.match(this.regexp):!1}};lb.exports=ob});var He=x((c$,fb)=>{u();var EO=wr(),OO=xr(),TO=Ni(),RO=_e(),ub=class extends EO{static save(e,t){let i=t.prop,n=[];for(let s in t._autoprefixerValues){let a=t._autoprefixerValues[s];if(a===t.value)continue;let o,l=TO.prefix(i);if(l==="-pie-")continue;if(l===s){o=t.value=a,n.push(o);continue}let c=e.prefixed(i,s),f=t.parent;if(!f.every(b=>b.prop!==c)){n.push(o);continue}let d=a.replace(/\s+/," ");if(f.some(b=>b.prop===t.prop&&b.value.replace(/\s+/," ")===d)){n.push(o);continue}let h=this.clone(t,{value:a});o=t.parent.insertBefore(t,h),n.push(o)}return n}check(e){let t=e.value;return t.includes(this.name)?!!t.match(this.regexp()):!1}regexp(){return this.regexpCache||(this.regexpCache=RO.regexp(this.name))}replace(e,t){return e.replace(this.regexp(),`$1${t}$2`)}value(e){return e.raws.value&&e.raws.value.value===e.value?e.raws.value.raw:e.value}add(e,t){e._autoprefixerValues||(e._autoprefixerValues={});let i=e._autoprefixerValues[t]||this.value(e),n;do if(n=i,i=this.replace(i,t),i===!1)return;while(i!==n);e._autoprefixerValues[t]=i}old(e){return new OO(this.name,e+this.name)}};fb.exports=ub});var Bt=x((p$,cb)=>{u();cb.exports={}});var ou=x((d$,hb)=>{u();var pb=Ms(),PO=He(),IO=Bt().insertAreas,DO=/(^|[^-])linear-gradient\(\s*(top|left|right|bottom)/i,qO=/(^|[^-])radial-gradient\(\s*\d+(\w*|%)\s+\d+(\w*|%)\s*,/i,$O=/(!\s*)?autoprefixer:\s*ignore\s+next/i,LO=/(!\s*)?autoprefixer\s*grid:\s*(on|off|(no-)?autoplace)/i,MO=["width","height","min-width","max-width","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size"];function au(r){return r.parent.some(e=>e.prop==="grid-template"||e.prop==="grid-template-areas")}function NO(r){let e=r.parent.some(i=>i.prop==="grid-template-rows"),t=r.parent.some(i=>i.prop==="grid-template-columns");return e&&t}var db=class{constructor(e){this.prefixes=e}add(e,t){let i=this.prefixes.add["@resolution"],n=this.prefixes.add["@keyframes"],s=this.prefixes.add["@viewport"],a=this.prefixes.add["@supports"];e.walkAtRules(f=>{if(f.name==="keyframes"){if(!this.disabled(f,t))return n&&n.process(f)}else if(f.name==="viewport"){if(!this.disabled(f,t))return s&&s.process(f)}else if(f.name==="supports"){if(this.prefixes.options.supports!==!1&&!this.disabled(f,t))return a.process(f)}else if(f.name==="media"&&f.params.includes("-resolution")&&!this.disabled(f,t))return i&&i.process(f)}),e.walkRules(f=>{if(!this.disabled(f,t))return this.prefixes.add.selectors.map(d=>d.process(f,t))});function o(f){return f.parent.nodes.some(d=>{if(d.type!=="decl")return!1;let p=d.prop==="display"&&/(inline-)?grid/.test(d.value),h=d.prop.startsWith("grid-template"),b=/^grid-([A-z]+-)?gap/.test(d.prop);return p||h||b})}function l(f){return f.parent.some(d=>d.prop==="display"&&/(inline-)?flex/.test(d.value))}let c=this.gridStatus(e,t)&&this.prefixes.add["grid-area"]&&this.prefixes.add["grid-area"].prefixes;return e.walkDecls(f=>{if(this.disabledDecl(f,t))return;let d=f.parent,p=f.prop,h=f.value;if(p==="grid-row-span"){t.warn("grid-row-span is not part of final Grid Layout. Use grid-row.",{node:f});return}else if(p==="grid-column-span"){t.warn("grid-column-span is not part of final Grid Layout. Use grid-column.",{node:f});return}else if(p==="display"&&h==="box"){t.warn("You should write display: flex by final spec instead of display: box",{node:f});return}else if(p==="text-emphasis-position")(h==="under"||h==="over")&&t.warn("You should use 2 values for text-emphasis-position For example, `under left` instead of just `under`.",{node:f});else if(/^(align|justify|place)-(items|content)$/.test(p)&&l(f))(h==="start"||h==="end")&&t.warn(`${h} value has mixed support, consider using flex-${h} instead`,{node:f});else if(p==="text-decoration-skip"&&h==="ink")t.warn("Replace text-decoration-skip: ink to text-decoration-skip-ink: auto, because spec had been changed",{node:f});else{if(c&&this.gridStatus(f,t))if(f.value==="subgrid"&&t.warn("IE does not support subgrid",{node:f}),/^(align|justify|place)-items$/.test(p)&&o(f)){let v=p.replace("-items","-self");t.warn(`IE does not support ${p} on grid containers. Try using ${v} on child elements instead: ${f.parent.selector} > * { ${v}: ${f.value} }`,{node:f})}else if(/^(align|justify|place)-content$/.test(p)&&o(f))t.warn(`IE does not support ${f.prop} on grid containers`,{node:f});else if(p==="display"&&f.value==="contents"){t.warn("Please do not use display: contents; if you have grid setting enabled",{node:f});return}else if(f.prop==="grid-gap"){let v=this.gridStatus(f,t);v==="autoplace"&&!NO(f)&&!au(f)?t.warn("grid-gap only works if grid-template(-areas) is being used or both rows and columns have been declared and cells have not been manually placed inside the explicit grid",{node:f}):(v===!0||v==="no-autoplace")&&!au(f)&&t.warn("grid-gap only works if grid-template(-areas) is being used",{node:f})}else if(p==="grid-auto-columns"){t.warn("grid-auto-columns is not supported by IE",{node:f});return}else if(p==="grid-auto-rows"){t.warn("grid-auto-rows is not supported by IE",{node:f});return}else if(p==="grid-auto-flow"){let v=d.some(w=>w.prop==="grid-template-rows"),y=d.some(w=>w.prop==="grid-template-columns");au(f)?t.warn("grid-auto-flow is not supported by IE",{node:f}):h.includes("dense")?t.warn("grid-auto-flow: dense is not supported by IE",{node:f}):!v&&!y&&t.warn("grid-auto-flow works only if grid-template-rows and grid-template-columns are present in the same rule",{node:f});return}else if(h.includes("auto-fit")){t.warn("auto-fit value is not supported by IE",{node:f,word:"auto-fit"});return}else if(h.includes("auto-fill")){t.warn("auto-fill value is not supported by IE",{node:f,word:"auto-fill"});return}else p.startsWith("grid-template")&&h.includes("[")&&t.warn("Autoprefixer currently does not support line names. Try using grid-template-areas instead.",{node:f,word:"["});if(h.includes("radial-gradient"))if(qO.test(f.value))t.warn("Gradient has outdated direction syntax. New syntax is like `closest-side at 0 0` instead of `0 0, closest-side`.",{node:f});else{let v=pb(h);for(let y of v.nodes)if(y.type==="function"&&y.value==="radial-gradient")for(let w of y.nodes)w.type==="word"&&(w.value==="cover"?t.warn("Gradient has outdated direction syntax. Replace `cover` to `farthest-corner`.",{node:f}):w.value==="contain"&&t.warn("Gradient has outdated direction syntax. Replace `contain` to `closest-side`.",{node:f}))}h.includes("linear-gradient")&&DO.test(h)&&t.warn("Gradient has outdated direction syntax. New syntax is like `to left` instead of `right`.",{node:f})}MO.includes(f.prop)&&(f.value.includes("-fill-available")||(f.value.includes("fill-available")?t.warn("Replace fill-available to stretch, because spec had been changed",{node:f}):f.value.includes("fill")&&pb(h).nodes.some(y=>y.type==="word"&&y.value==="fill")&&t.warn("Replace fill to stretch, because spec had been changed",{node:f})));let b;if(f.prop==="transition"||f.prop==="transition-property")return this.prefixes.transition.add(f,t);if(f.prop==="align-self"){if(this.displayType(f)!=="grid"&&this.prefixes.options.flexbox!==!1&&(b=this.prefixes.add["align-self"],b&&b.prefixes&&b.process(f)),this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-row-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="justify-self"){if(this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-column-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="place-self"){if(b=this.prefixes.add["place-self"],b&&b.prefixes&&this.gridStatus(f,t)!==!1)return b.process(f,t)}else if(b=this.prefixes.add[f.prop],b&&b.prefixes)return b.process(f,t)}),this.gridStatus(e,t)&&IO(e,this.disabled),e.walkDecls(f=>{if(this.disabledValue(f,t))return;let d=this.prefixes.unprefixed(f.prop),p=this.prefixes.values("add",d);if(Array.isArray(p))for(let h of p)h.process&&h.process(f,t);PO.save(this.prefixes,f)})}remove(e,t){let i=this.prefixes.remove["@resolution"];e.walkAtRules((n,s)=>{this.prefixes.remove[`@${n.name}`]?this.disabled(n,t)||n.parent.removeChild(s):n.name==="media"&&n.params.includes("-resolution")&&i&&i.clean(n)});for(let n of this.prefixes.remove.selectors)e.walkRules((s,a)=>{n.check(s)&&(this.disabled(s,t)||s.parent.removeChild(a))});return e.walkDecls((n,s)=>{if(this.disabled(n,t))return;let a=n.parent,o=this.prefixes.unprefixed(n.prop);if((n.prop==="transition"||n.prop==="transition-property")&&this.prefixes.transition.remove(n),this.prefixes.remove[n.prop]&&this.prefixes.remove[n.prop].remove){let l=this.prefixes.group(n).down(c=>this.prefixes.normalize(c.prop)===o);if(o==="flex-flow"&&(l=!0),n.prop==="-webkit-box-orient"){let c={"flex-direction":!0,"flex-flow":!0};if(!n.parent.some(f=>c[f.prop]))return}if(l&&!this.withHackValue(n)){n.raw("before").includes(` +`)&&this.reduceSpaces(n),a.removeChild(s);return}}for(let l of this.prefixes.values("remove",o)){if(!l.check||!l.check(n.value))continue;if(o=l.unprefixed,this.prefixes.group(n).down(f=>f.value.includes(o))){a.removeChild(s);return}}})}withHackValue(e){return e.prop==="-webkit-background-clip"&&e.value==="text"}disabledValue(e,t){return this.gridStatus(e,t)===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("grid")||this.prefixes.options.flexbox===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("flex")||e.type==="decl"&&e.prop==="content"?!0:this.disabled(e,t)}disabledDecl(e,t){if(this.gridStatus(e,t)===!1&&e.type==="decl"&&(e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.prefixes.options.flexbox===!1&&e.type==="decl"){let i=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||i.includes(e.prop))return!0}return this.disabled(e,t)}disabled(e,t){if(!e)return!1;if(e._autoprefixerDisabled!==void 0)return e._autoprefixerDisabled;if(e.parent){let n=e.prev();if(n&&n.type==="comment"&&$O.test(n.text))return e._autoprefixerDisabled=!0,e._autoprefixerSelfDisabled=!0,!0}let i=null;if(e.nodes){let n;e.each(s=>{s.type==="comment"&&/(!\s*)?autoprefixer:\s*(off|on)/i.test(s.text)&&(typeof n!="undefined"?t.warn("Second Autoprefixer control comment was ignored. Autoprefixer applies control comment to whole block, not to next rules.",{node:s}):n=/on/i.test(s.text))}),n!==void 0&&(i=!n)}if(!e.nodes||i===null)if(e.parent){let n=this.disabled(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else i=!1;return e._autoprefixerDisabled=i,i}reduceSpaces(e){let t=!1;if(this.prefixes.group(e).up(()=>(t=!0,!0)),t)return;let i=e.raw("before").split(` +`),n=i[i.length-1].length,s=!1;this.prefixes.group(e).down(a=>{i=a.raw("before").split(` +`);let o=i.length-1;i[o].length>n&&(s===!1&&(s=i[o].length-n),i[o]=i[o].slice(0,-s),a.raws.before=i.join(` +`))})}displayType(e){for(let t of e.parent.nodes)if(t.prop==="display"){if(t.value.includes("flex"))return"flex";if(t.value.includes("grid"))return"grid"}return!1}gridStatus(e,t){if(!e)return!1;if(e._autoprefixerGridStatus!==void 0)return e._autoprefixerGridStatus;let i=null;if(e.nodes){let n;e.each(s=>{if(s.type==="comment"&&LO.test(s.text)){let a=/:\s*autoplace/i.test(s.text),o=/no-autoplace/i.test(s.text);typeof n!="undefined"?t.warn("Second Autoprefixer grid control comment was ignored. Autoprefixer applies control comments to the whole block, not to the next rules.",{node:s}):a?n="autoplace":o?n=!0:n=/on/i.test(s.text)}}),n!==void 0&&(i=n)}if(e.type==="atrule"&&e.name==="supports"){let n=e.params;n.includes("grid")&&n.includes("auto")&&(i=!1)}if(!e.nodes||i===null)if(e.parent){let n=this.gridStatus(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else typeof this.prefixes.options.grid!="undefined"?i=this.prefixes.options.grid:typeof m.env.AUTOPREFIXER_GRID!="undefined"?m.env.AUTOPREFIXER_GRID==="autoplace"?i="autoplace":i=!0:i=!1;return e._autoprefixerGridStatus=i,i}};hb.exports=db});var gb=x((h$,mb)=>{u();mb.exports={A:{A:{"2":"K E F G A B JC"},B:{"1":"C L M H N D O P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I"},C:{"1":"2 3 4 5 6 7 8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 KC zB J K E F G A B C L M H N D O k l LC MC"},D:{"1":"8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 2 3 4 5 6 7 J K E F G A B C L M H N D O k l"},E:{"1":"G A B C L M H D RC 6B vB wB 7B SC TC 8B 9B xB AC yB BC CC DC EC FC GC UC","2":"0 J K E F NC 5B OC PC QC"},F:{"1":"1 2 3 4 5 6 7 8 9 H N D O k l AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB dB eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j wB","2":"G B C VC WC XC YC vB HC ZC"},G:{"1":"D fC gC hC iC jC kC lC mC nC oC pC qC rC sC tC 8B 9B xB AC yB BC CC DC EC FC GC","2":"F 5B aC IC bC cC dC eC"},H:{"1":"uC"},I:{"1":"I zC 0C","2":"zB J vC wC xC yC IC"},J:{"2":"E A"},K:{"1":"m","2":"A B C vB HC wB"},L:{"1":"I"},M:{"1":"uB"},N:{"2":"A B"},O:{"1":"xB"},P:{"1":"J k l 1C 2C 3C 4C 5C 6B 6C 7C 8C 9C AD yB BD CD DD"},Q:{"1":"7B"},R:{"1":"ED"},S:{"1":"FD GD"}},B:4,C:"CSS Feature Queries"}});var vb=x((m$,wb)=>{u();function yb(r){return r[r.length-1]}var bb={parse(r){let e=[""],t=[e];for(let i of r){if(i==="("){e=[""],yb(t).push(e),t.push(e);continue}if(i===")"){t.pop(),e=yb(t),e.push("");continue}e[e.length-1]+=i}return t[0]},stringify(r){let e="";for(let t of r){if(typeof t=="object"){e+=`(${bb.stringify(t)})`;continue}e+=t}return e}};wb.exports=bb});var Cb=x((g$,Ab)=>{u();var BO=gb(),{feature:FO}=(Ps(),Rs),{parse:jO}=$e(),zO=Mt(),lu=vb(),UO=He(),VO=_e(),xb=FO(BO),kb=[];for(let r in xb.stats){let e=xb.stats[r];for(let t in e){let i=e[t];/y/.test(i)&&kb.push(r+" "+t)}}var Sb=class{constructor(e,t){this.Prefixes=e,this.all=t}prefixer(){if(this.prefixerCache)return this.prefixerCache;let e=this.all.browsers.selected.filter(i=>kb.includes(i)),t=new zO(this.all.browsers.data,e,this.all.options);return this.prefixerCache=new this.Prefixes(this.all.data,t,this.all.options),this.prefixerCache}parse(e){let t=e.split(":"),i=t[0],n=t[1];return n||(n=""),[i.trim(),n.trim()]}virtual(e){let[t,i]=this.parse(e),n=jO("a{}").first;return n.append({prop:t,value:i,raws:{before:""}}),n}prefixed(e){let t=this.virtual(e);if(this.disabled(t.first))return t.nodes;let i={warn:()=>null},n=this.prefixer().add[t.first.prop];n&&n.process&&n.process(t.first,i);for(let s of t.nodes){for(let a of this.prefixer().values("add",t.first.prop))a.process(s);UO.save(this.all,s)}return t.nodes}isNot(e){return typeof e=="string"&&/not\s*/i.test(e)}isOr(e){return typeof e=="string"&&/\s*or\s*/i.test(e)}isProp(e){return typeof e=="object"&&e.length===1&&typeof e[0]=="string"}isHack(e,t){return!new RegExp(`(\\(|\\s)${VO.escapeRegexp(t)}:`).test(e)}toRemove(e,t){let[i,n]=this.parse(e),s=this.all.unprefixed(i),a=this.all.cleaner();if(a.remove[i]&&a.remove[i].remove&&!this.isHack(t,s))return!0;for(let o of a.values("remove",s))if(o.check(n))return!0;return!1}remove(e,t){let i=0;for(;itypeof t!="object"?t:t.length===1&&typeof t[0]=="object"?this.cleanBrackets(t[0]):this.cleanBrackets(t))}convert(e){let t=[""];for(let i of e)t.push([`${i.prop}: ${i.value}`]),t.push(" or ");return t[t.length-1]="",t}normalize(e){if(typeof e!="object")return e;if(e=e.filter(t=>t!==""),typeof e[0]=="string"){let t=e[0].trim();if(t.includes(":")||t==="selector"||t==="not selector")return[lu.stringify(e)]}return e.map(t=>this.normalize(t))}add(e,t){return e.map(i=>{if(this.isProp(i)){let n=this.prefixed(i[0]);return n.length>1?this.convert(n):i}return typeof i=="object"?this.add(i,t):i})}process(e){let t=lu.parse(e.params);t=this.normalize(t),t=this.remove(t,e.params),t=this.add(t,e.params),t=this.cleanBrackets(t),e.params=lu.stringify(t)}disabled(e){if(!this.all.options.grid&&(e.prop==="display"&&e.value.includes("grid")||e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.all.options.flexbox===!1){if(e.prop==="display"&&e.value.includes("flex"))return!0;let t=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||t.includes(e.prop))return!0}return!1}};Ab.exports=Sb});var Ob=x((y$,Eb)=>{u();var _b=class{constructor(e,t){this.prefix=t,this.prefixed=e.prefixed(this.prefix),this.regexp=e.regexp(this.prefix),this.prefixeds=e.possible().map(i=>[e.prefixed(i),e.regexp(i)]),this.unprefixed=e.name,this.nameRegexp=e.regexp()}isHack(e){let t=e.parent.index(e)+1,i=e.parent.nodes;for(;t{u();var{list:HO}=$e(),WO=Ob(),GO=wr(),QO=Mt(),YO=_e(),Tb=class extends GO{constructor(e,t,i){super(e,t,i);this.regexpCache=new Map}check(e){return e.selector.includes(this.name)?!!e.selector.match(this.regexp()):!1}prefixed(e){return this.name.replace(/^(\W*)/,`$1${e}`)}regexp(e){if(!this.regexpCache.has(e)){let t=e?this.prefixed(e):this.name;this.regexpCache.set(e,new RegExp(`(^|[^:"'=])${YO.escapeRegexp(t)}`,"gi"))}return this.regexpCache.get(e)}possible(){return QO.prefixes()}prefixeds(e){if(e._autoprefixerPrefixeds){if(e._autoprefixerPrefixeds[this.name])return e._autoprefixerPrefixeds}else e._autoprefixerPrefixeds={};let t={};if(e.selector.includes(",")){let n=HO.comma(e.selector).filter(s=>s.includes(this.name));for(let s of this.possible())t[s]=n.map(a=>this.replace(a,s)).join(", ")}else for(let i of this.possible())t[i]=this.replace(e.selector,i);return e._autoprefixerPrefixeds[this.name]=t,e._autoprefixerPrefixeds}already(e,t,i){let n=e.parent.index(e)-1;for(;n>=0;){let s=e.parent.nodes[n];if(s.type!=="rule")return!1;let a=!1;for(let o in t[this.name]){let l=t[this.name][o];if(s.selector===l){if(i===o)return!0;a=!0;break}}if(!a)return!1;n-=1}return!1}replace(e,t){return e.replace(this.regexp(),`$1${this.prefixed(t)}`)}add(e,t){let i=this.prefixeds(e);if(this.already(e,i,t))return;let n=this.clone(e,{selector:i[this.name][t]});e.parent.insertBefore(e,n)}old(e){return new WO(this,e)}};Rb.exports=Tb});var Db=x((w$,Ib)=>{u();var KO=wr(),Pb=class extends KO{add(e,t){let i=t+e.name;if(e.parent.some(a=>a.name===i&&a.params===e.params))return;let s=this.clone(e,{name:i});return e.parent.insertBefore(e,s)}process(e){let t=this.parentPrefix(e);for(let i of this.prefixes)(!t||t===i)&&this.add(e,i)}};Ib.exports=Pb});var $b=x((v$,qb)=>{u();var XO=kr(),uu=class extends XO{prefixed(e){return e==="-webkit-"?":-webkit-full-screen":e==="-moz-"?":-moz-full-screen":`:${e}fullscreen`}};uu.names=[":fullscreen"];qb.exports=uu});var Mb=x((x$,Lb)=>{u();var ZO=kr(),fu=class extends ZO{possible(){return super.possible().concat(["-moz- old","-ms- old"])}prefixed(e){return e==="-webkit-"?"::-webkit-input-placeholder":e==="-ms-"?"::-ms-input-placeholder":e==="-ms- old"?":-ms-input-placeholder":e==="-moz- old"?":-moz-placeholder":`::${e}placeholder`}};fu.names=["::placeholder"];Lb.exports=fu});var Bb=x((k$,Nb)=>{u();var JO=kr(),cu=class extends JO{prefixed(e){return e==="-ms-"?":-ms-input-placeholder":`:${e}placeholder-shown`}};cu.names=[":placeholder-shown"];Nb.exports=cu});var jb=x((S$,Fb)=>{u();var eT=kr(),tT=_e(),pu=class extends eT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=tT.uniq(this.prefixes.map(n=>"-webkit-")))}prefixed(e){return e==="-webkit-"?"::-webkit-file-upload-button":`::${e}file-selector-button`}};pu.names=["::file-selector-button"];Fb.exports=pu});var Pe=x((A$,zb)=>{u();zb.exports=function(r){let e;return r==="-webkit- 2009"||r==="-moz-"?e=2009:r==="-ms-"?e=2012:r==="-webkit-"&&(e="final"),r==="-webkit- 2009"&&(r="-webkit-"),[e,r]}});var Wb=x((C$,Hb)=>{u();var Ub=$e().list,Vb=Pe(),rT=j(),Sr=class extends rT{prefixed(e,t){let i;return[i,t]=Vb(t),i===2009?t+"box-flex":super.prefixed(e,t)}normalize(){return"flex"}set(e,t){let i=Vb(t)[0];if(i===2009)return e.value=Ub.space(e.value)[0],e.value=Sr.oldValues[e.value]||e.value,super.set(e,t);if(i===2012){let n=Ub.space(e.value);n.length===3&&n[2]==="0"&&(e.value=n.slice(0,2).concat("0px").join(" "))}return super.set(e,t)}};Sr.names=["flex","box-flex"];Sr.oldValues={auto:"1",none:"0"};Hb.exports=Sr});var Yb=x((_$,Qb)=>{u();var Gb=Pe(),iT=j(),du=class extends iT{prefixed(e,t){let i;return[i,t]=Gb(t),i===2009?t+"box-ordinal-group":i===2012?t+"flex-order":super.prefixed(e,t)}normalize(){return"order"}set(e,t){return Gb(t)[0]===2009&&/\d/.test(e.value)?(e.value=(parseInt(e.value)+1).toString(),super.set(e,t)):super.set(e,t)}};du.names=["order","flex-order","box-ordinal-group"];Qb.exports=du});var Xb=x((E$,Kb)=>{u();var nT=j(),hu=class extends nT{check(e){let t=e.value;return!t.toLowerCase().includes("alpha(")&&!t.includes("DXImageTransform.Microsoft")&&!t.includes("data:image/svg+xml")}};hu.names=["filter"];Kb.exports=hu});var Jb=x((O$,Zb)=>{u();var sT=j(),mu=class extends sT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=this.clone(e),a=e.prop.replace(/end$/,"start"),o=t+e.prop.replace(/end$/,"span");if(!e.parent.some(l=>l.prop===o)){if(s.prop=o,e.value.includes("span"))s.value=e.value.replace(/span\s/i,"");else{let l;if(e.parent.walkDecls(a,c=>{l=c}),l){let c=Number(e.value)-Number(l.value)+"";s.value=c}else e.warn(n,`Can not prefix ${e.prop} (${a} is not found)`)}e.cloneBefore(s)}}};mu.names=["grid-row-end","grid-column-end"];Zb.exports=mu});var tw=x((T$,ew)=>{u();var aT=j(),gu=class extends aT{check(e){return!e.value.split(/\s+/).some(t=>{let i=t.toLowerCase();return i==="reverse"||i==="alternate-reverse"})}};gu.names=["animation","animation-direction"];ew.exports=gu});var iw=x((R$,rw)=>{u();var oT=Pe(),lT=j(),yu=class extends lT{insert(e,t,i){let n;if([n,t]=oT(t),n!==2009)return super.insert(e,t,i);let s=e.value.split(/\s+/).filter(d=>d!=="wrap"&&d!=="nowrap"&&"wrap-reverse");if(s.length===0||e.parent.some(d=>d.prop===t+"box-orient"||d.prop===t+"box-direction"))return;let o=s[0],l=o.includes("row")?"horizontal":"vertical",c=o.includes("reverse")?"reverse":"normal",f=this.clone(e);return f.prop=t+"box-orient",f.value=l,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f),f=this.clone(e),f.prop=t+"box-direction",f.value=c,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f)}};yu.names=["flex-flow","box-direction","box-orient"];rw.exports=yu});var sw=x((P$,nw)=>{u();var uT=Pe(),fT=j(),bu=class extends fT{normalize(){return"flex"}prefixed(e,t){let i;return[i,t]=uT(t),i===2009?t+"box-flex":i===2012?t+"flex-positive":super.prefixed(e,t)}};bu.names=["flex-grow","flex-positive"];nw.exports=bu});var ow=x((I$,aw)=>{u();var cT=Pe(),pT=j(),wu=class extends pT{set(e,t){if(cT(t)[0]!==2009)return super.set(e,t)}};wu.names=["flex-wrap"];aw.exports=wu});var uw=x((D$,lw)=>{u();var dT=j(),Ar=Bt(),vu=class extends dT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=Ar.parse(e),[a,o]=Ar.translate(s,0,2),[l,c]=Ar.translate(s,1,3);[["grid-row",a],["grid-row-span",o],["grid-column",l],["grid-column-span",c]].forEach(([f,d])=>{Ar.insertDecl(e,f,d)}),Ar.warnTemplateSelectorNotFound(e,n),Ar.warnIfGridRowColumnExists(e,n)}};vu.names=["grid-area"];lw.exports=vu});var cw=x((q$,fw)=>{u();var hT=j(),Bi=Bt(),xu=class extends hT{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(a=>a.prop==="-ms-grid-row-align"))return;let[[n,s]]=Bi.parse(e);s?(Bi.insertDecl(e,"grid-row-align",n),Bi.insertDecl(e,"grid-column-align",s)):(Bi.insertDecl(e,"grid-row-align",n),Bi.insertDecl(e,"grid-column-align",n))}};xu.names=["place-self"];fw.exports=xu});var dw=x(($$,pw)=>{u();var mT=j(),ku=class extends mT{check(e){let t=e.value;return!t.includes("/")||t.includes("span")}normalize(e){return e.replace("-start","")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-ms-"&&(i=i.replace("-start","")),i}};ku.names=["grid-row-start","grid-column-start"];pw.exports=ku});var gw=x((L$,mw)=>{u();var hw=Pe(),gT=j(),Cr=class extends gT{check(e){return e.parent&&!e.parent.some(t=>t.prop&&t.prop.startsWith("grid-"))}prefixed(e,t){let i;return[i,t]=hw(t),i===2012?t+"flex-item-align":super.prefixed(e,t)}normalize(){return"align-self"}set(e,t){let i=hw(t)[0];if(i===2012)return e.value=Cr.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Cr.names=["align-self","flex-item-align"];Cr.oldValues={"flex-end":"end","flex-start":"start"};mw.exports=Cr});var bw=x((M$,yw)=>{u();var yT=j(),bT=_e(),Su=class extends yT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=bT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Su.names=["appearance"];yw.exports=Su});var xw=x((N$,vw)=>{u();var ww=Pe(),wT=j(),Au=class extends wT{normalize(){return"flex-basis"}prefixed(e,t){let i;return[i,t]=ww(t),i===2012?t+"flex-preferred-size":super.prefixed(e,t)}set(e,t){let i;if([i,t]=ww(t),i===2012||i==="final")return super.set(e,t)}};Au.names=["flex-basis","flex-preferred-size"];vw.exports=Au});var Sw=x((B$,kw)=>{u();var vT=j(),Cu=class extends vT{normalize(){return this.name.replace("box-image","border")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-webkit-"&&(i=i.replace("border","box-image")),i}};Cu.names=["mask-border","mask-border-source","mask-border-slice","mask-border-width","mask-border-outset","mask-border-repeat","mask-box-image","mask-box-image-source","mask-box-image-slice","mask-box-image-width","mask-box-image-outset","mask-box-image-repeat"];kw.exports=Cu});var Cw=x((F$,Aw)=>{u();var xT=j(),lt=class extends xT{insert(e,t,i){let n=e.prop==="mask-composite",s;n?s=e.value.split(","):s=e.value.match(lt.regexp)||[],s=s.map(c=>c.trim()).filter(c=>c);let a=s.length,o;if(a&&(o=this.clone(e),o.value=s.map(c=>lt.oldValues[c]||c).join(", "),s.includes("intersect")&&(o.value+=", xor"),o.prop=t+"mask-composite"),n)return a?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):void 0;let l=this.clone(e);return l.prop=t+l.prop,a&&(l.value=l.value.replace(lt.regexp,"")),this.needCascade(e)&&(l.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,l),a?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):e}};lt.names=["mask","mask-composite"];lt.oldValues={add:"source-over",subtract:"source-out",intersect:"source-in",exclude:"xor"};lt.regexp=new RegExp(`\\s+(${Object.keys(lt.oldValues).join("|")})\\b(?!\\))\\s*(?=[,])`,"ig");Aw.exports=lt});var Ow=x((j$,Ew)=>{u();var _w=Pe(),kT=j(),_r=class extends kT{prefixed(e,t){let i;return[i,t]=_w(t),i===2009?t+"box-align":i===2012?t+"flex-align":super.prefixed(e,t)}normalize(){return"align-items"}set(e,t){let i=_w(t)[0];return(i===2009||i===2012)&&(e.value=_r.oldValues[e.value]||e.value),super.set(e,t)}};_r.names=["align-items","flex-align","box-align"];_r.oldValues={"flex-end":"end","flex-start":"start"};Ew.exports=_r});var Rw=x((z$,Tw)=>{u();var ST=j(),_u=class extends ST{set(e,t){return t==="-ms-"&&e.value==="contain"&&(e.value="element"),super.set(e,t)}insert(e,t,i){if(!(e.value==="all"&&t==="-ms-"))return super.insert(e,t,i)}};_u.names=["user-select"];Tw.exports=_u});var Dw=x((U$,Iw)=>{u();var Pw=Pe(),AT=j(),Eu=class extends AT{normalize(){return"flex-shrink"}prefixed(e,t){let i;return[i,t]=Pw(t),i===2012?t+"flex-negative":super.prefixed(e,t)}set(e,t){let i;if([i,t]=Pw(t),i===2012||i==="final")return super.set(e,t)}};Eu.names=["flex-shrink","flex-negative"];Iw.exports=Eu});var $w=x((V$,qw)=>{u();var CT=j(),Ou=class extends CT{prefixed(e,t){return`${t}column-${e}`}normalize(e){return e.includes("inside")?"break-inside":e.includes("before")?"break-before":"break-after"}set(e,t){return(e.prop==="break-inside"&&e.value==="avoid-column"||e.value==="avoid-page")&&(e.value="avoid"),super.set(e,t)}insert(e,t,i){if(e.prop!=="break-inside")return super.insert(e,t,i);if(!(/region/i.test(e.value)||/page/i.test(e.value)))return super.insert(e,t,i)}};Ou.names=["break-inside","page-break-inside","column-break-inside","break-before","page-break-before","column-break-before","break-after","page-break-after","column-break-after"];qw.exports=Ou});var Mw=x((H$,Lw)=>{u();var _T=j(),Tu=class extends _T{prefixed(e,t){return t+"print-color-adjust"}normalize(){return"color-adjust"}};Tu.names=["color-adjust","print-color-adjust"];Lw.exports=Tu});var Bw=x((W$,Nw)=>{u();var ET=j(),Er=class extends ET{insert(e,t,i){if(t==="-ms-"){let n=this.set(this.clone(e),t);this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t));let s="ltr";return e.parent.nodes.forEach(a=>{a.prop==="direction"&&(a.value==="rtl"||a.value==="ltr")&&(s=a.value)}),n.value=Er.msValues[s][e.value]||e.value,e.parent.insertBefore(e,n)}return super.insert(e,t,i)}};Er.names=["writing-mode"];Er.msValues={ltr:{"horizontal-tb":"lr-tb","vertical-rl":"tb-rl","vertical-lr":"tb-lr"},rtl:{"horizontal-tb":"rl-tb","vertical-rl":"bt-rl","vertical-lr":"bt-lr"}};Nw.exports=Er});var jw=x((G$,Fw)=>{u();var OT=j(),Ru=class extends OT{set(e,t){return e.value=e.value.replace(/\s+fill(\s)/,"$1"),super.set(e,t)}};Ru.names=["border-image"];Fw.exports=Ru});var Vw=x((Q$,Uw)=>{u();var zw=Pe(),TT=j(),Or=class extends TT{prefixed(e,t){let i;return[i,t]=zw(t),i===2012?t+"flex-line-pack":super.prefixed(e,t)}normalize(){return"align-content"}set(e,t){let i=zw(t)[0];if(i===2012)return e.value=Or.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Or.names=["align-content","flex-line-pack"];Or.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};Uw.exports=Or});var Ww=x((Y$,Hw)=>{u();var RT=j(),We=class extends RT{prefixed(e,t){return t==="-moz-"?t+(We.toMozilla[e]||e):super.prefixed(e,t)}normalize(e){return We.toNormal[e]||e}};We.names=["border-radius"];We.toMozilla={};We.toNormal={};for(let r of["top","bottom"])for(let e of["left","right"]){let t=`border-${r}-${e}-radius`,i=`border-radius-${r}${e}`;We.names.push(t),We.names.push(i),We.toMozilla[t]=i,We.toNormal[i]=t}Hw.exports=We});var Qw=x((K$,Gw)=>{u();var PT=j(),Pu=class extends PT{prefixed(e,t){return e.includes("-start")?t+e.replace("-block-start","-before"):t+e.replace("-block-end","-after")}normalize(e){return e.includes("-before")?e.replace("-before","-block-start"):e.replace("-after","-block-end")}};Pu.names=["border-block-start","border-block-end","margin-block-start","margin-block-end","padding-block-start","padding-block-end","border-before","border-after","margin-before","margin-after","padding-before","padding-after"];Gw.exports=Pu});var Kw=x((X$,Yw)=>{u();var IT=j(),{parseTemplate:DT,warnMissedAreas:qT,getGridGap:$T,warnGridGap:LT,inheritGridGap:MT}=Bt(),Iu=class extends IT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(h=>h.prop==="-ms-grid-rows"))return;let s=$T(e),a=MT(e,s),{rows:o,columns:l,areas:c}=DT({decl:e,gap:a||s}),f=Object.keys(c).length>0,d=Boolean(o),p=Boolean(l);return LT({gap:s,hasColumns:p,decl:e,result:n}),qT(c,e,n),(d&&p||f)&&e.cloneBefore({prop:"-ms-grid-rows",value:o,raws:{}}),p&&e.cloneBefore({prop:"-ms-grid-columns",value:l,raws:{}}),e}};Iu.names=["grid-template"];Yw.exports=Iu});var Zw=x((Z$,Xw)=>{u();var NT=j(),Du=class extends NT{prefixed(e,t){return t+e.replace("-inline","")}normalize(e){return e.replace(/(margin|padding|border)-(start|end)/,"$1-inline-$2")}};Du.names=["border-inline-start","border-inline-end","margin-inline-start","margin-inline-end","padding-inline-start","padding-inline-end","border-start","border-end","margin-start","margin-end","padding-start","padding-end"];Xw.exports=Du});var e0=x((J$,Jw)=>{u();var BT=j(),qu=class extends BT{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-row-align"}normalize(){return"align-self"}};qu.names=["grid-row-align"];Jw.exports=qu});var r0=x((eL,t0)=>{u();var FT=j(),Tr=class extends FT{keyframeParents(e){let{parent:t}=e;for(;t;){if(t.type==="atrule"&&t.name==="keyframes")return!0;({parent:t}=t)}return!1}contain3d(e){if(e.prop==="transform-origin")return!1;for(let t of Tr.functions3d)if(e.value.includes(`${t}(`))return!0;return!1}set(e,t){return e=super.set(e,t),t==="-ms-"&&(e.value=e.value.replace(/rotatez/gi,"rotate")),e}insert(e,t,i){if(t==="-ms-"){if(!this.contain3d(e)&&!this.keyframeParents(e))return super.insert(e,t,i)}else if(t==="-o-"){if(!this.contain3d(e))return super.insert(e,t,i)}else return super.insert(e,t,i)}};Tr.names=["transform","transform-origin"];Tr.functions3d=["matrix3d","translate3d","translateZ","scale3d","scaleZ","rotate3d","rotateX","rotateY","perspective"];t0.exports=Tr});var s0=x((tL,n0)=>{u();var i0=Pe(),jT=j(),$u=class extends jT{normalize(){return"flex-direction"}insert(e,t,i){let n;if([n,t]=i0(t),n!==2009)return super.insert(e,t,i);if(e.parent.some(f=>f.prop===t+"box-orient"||f.prop===t+"box-direction"))return;let a=e.value,o,l;a==="inherit"||a==="initial"||a==="unset"?(o=a,l=a):(o=a.includes("row")?"horizontal":"vertical",l=a.includes("reverse")?"reverse":"normal");let c=this.clone(e);return c.prop=t+"box-orient",c.value=o,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c),c=this.clone(e),c.prop=t+"box-direction",c.value=l,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c)}old(e,t){let i;return[i,t]=i0(t),i===2009?[t+"box-orient",t+"box-direction"]:super.old(e,t)}};$u.names=["flex-direction","box-direction","box-orient"];n0.exports=$u});var o0=x((rL,a0)=>{u();var zT=j(),Lu=class extends zT{check(e){return e.value==="pixelated"}prefixed(e,t){return t==="-ms-"?"-ms-interpolation-mode":super.prefixed(e,t)}set(e,t){return t!=="-ms-"?super.set(e,t):(e.prop="-ms-interpolation-mode",e.value="nearest-neighbor",e)}normalize(){return"image-rendering"}process(e,t){return super.process(e,t)}};Lu.names=["image-rendering","interpolation-mode"];a0.exports=Lu});var u0=x((iL,l0)=>{u();var UT=j(),VT=_e(),Mu=class extends UT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=VT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Mu.names=["backdrop-filter"];l0.exports=Mu});var c0=x((nL,f0)=>{u();var HT=j(),WT=_e(),Nu=class extends HT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=WT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}check(e){return e.value.toLowerCase()==="text"}};Nu.names=["background-clip"];f0.exports=Nu});var d0=x((sL,p0)=>{u();var GT=j(),QT=["none","underline","overline","line-through","blink","inherit","initial","unset"],Bu=class extends GT{check(e){return e.value.split(/\s+/).some(t=>!QT.includes(t))}};Bu.names=["text-decoration"];p0.exports=Bu});var g0=x((aL,m0)=>{u();var h0=Pe(),YT=j(),Rr=class extends YT{prefixed(e,t){let i;return[i,t]=h0(t),i===2009?t+"box-pack":i===2012?t+"flex-pack":super.prefixed(e,t)}normalize(){return"justify-content"}set(e,t){let i=h0(t)[0];if(i===2009||i===2012){let n=Rr.oldValues[e.value]||e.value;if(e.value=n,i!==2009||n!=="distribute")return super.set(e,t)}else if(i==="final")return super.set(e,t)}};Rr.names=["justify-content","flex-pack","box-pack"];Rr.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};m0.exports=Rr});var b0=x((oL,y0)=>{u();var KT=j(),Fu=class extends KT{set(e,t){let i=e.value.toLowerCase();return t==="-webkit-"&&!i.includes(" ")&&i!=="contain"&&i!=="cover"&&(e.value=e.value+" "+e.value),super.set(e,t)}};Fu.names=["background-size"];y0.exports=Fu});var v0=x((lL,w0)=>{u();var XT=j(),ju=Bt(),zu=class extends XT{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);let n=ju.parse(e),[s,a]=ju.translate(n,0,1);n[0]&&n[0].includes("span")&&(a=n[0].join("").replace(/\D/g,"")),[[e.prop,s],[`${e.prop}-span`,a]].forEach(([l,c])=>{ju.insertDecl(e,l,c)})}};zu.names=["grid-row","grid-column"];w0.exports=zu});var S0=x((uL,k0)=>{u();var ZT=j(),{prefixTrackProp:x0,prefixTrackValue:JT,autoplaceGridItems:eR,getGridGap:tR,inheritGridGap:rR}=Bt(),iR=ou(),Uu=class extends ZT{prefixed(e,t){return t==="-ms-"?x0({prop:e,prefix:t}):super.prefixed(e,t)}normalize(e){return e.replace(/^grid-(rows|columns)/,"grid-template-$1")}insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let{parent:s,prop:a,value:o}=e,l=a.includes("rows"),c=a.includes("columns"),f=s.some(k=>k.prop==="grid-template"||k.prop==="grid-template-areas");if(f&&l)return!1;let d=new iR({options:{}}),p=d.gridStatus(s,n),h=tR(e);h=rR(e,h)||h;let b=l?h.row:h.column;(p==="no-autoplace"||p===!0)&&!f&&(b=null);let v=JT({value:o,gap:b});e.cloneBefore({prop:x0({prop:a,prefix:t}),value:v});let y=s.nodes.find(k=>k.prop==="grid-auto-flow"),w="row";if(y&&!d.disabled(y,n)&&(w=y.value.trim()),p==="autoplace"){let k=s.nodes.find(E=>E.prop==="grid-template-rows");if(!k&&f)return;if(!k&&!f){e.warn(n,"Autoplacement does not work without grid-template-rows property");return}!s.nodes.find(E=>E.prop==="grid-template-columns")&&!f&&e.warn(n,"Autoplacement does not work without grid-template-columns property"),c&&!f&&eR(e,n,h,w)}}};Uu.names=["grid-template-rows","grid-template-columns","grid-rows","grid-columns"];k0.exports=Uu});var C0=x((fL,A0)=>{u();var nR=j(),Vu=class extends nR{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-column-align"}normalize(){return"justify-self"}};Vu.names=["grid-column-align"];A0.exports=Vu});var E0=x((cL,_0)=>{u();var sR=j(),Hu=class extends sR{prefixed(e,t){return t+"scroll-chaining"}normalize(){return"overscroll-behavior"}set(e,t){return e.value==="auto"?e.value="chained":(e.value==="none"||e.value==="contain")&&(e.value="none"),super.set(e,t)}};Hu.names=["overscroll-behavior","scroll-chaining"];_0.exports=Hu});var R0=x((pL,T0)=>{u();var aR=j(),{parseGridAreas:oR,warnMissedAreas:lR,prefixTrackProp:uR,prefixTrackValue:O0,getGridGap:fR,warnGridGap:cR,inheritGridGap:pR}=Bt();function dR(r){return r.trim().slice(1,-1).split(/["']\s*["']?/g)}var Wu=class extends aR{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=!1,a=!1,o=e.parent,l=fR(e);l=pR(e,l)||l,o.walkDecls(/-ms-grid-rows/,d=>d.remove()),o.walkDecls(/grid-template-(rows|columns)/,d=>{if(d.prop==="grid-template-rows"){a=!0;let{prop:p,value:h}=d;d.cloneBefore({prop:uR({prop:p,prefix:t}),value:O0({value:h,gap:l.row})})}else s=!0});let c=dR(e.value);s&&!a&&l.row&&c.length>1&&e.cloneBefore({prop:"-ms-grid-rows",value:O0({value:`repeat(${c.length}, auto)`,gap:l.row}),raws:{}}),cR({gap:l,hasColumns:s,decl:e,result:n});let f=oR({rows:c,gap:l});return lR(f,e,n),e}};Wu.names=["grid-template-areas"];T0.exports=Wu});var I0=x((dL,P0)=>{u();var hR=j(),Gu=class extends hR{set(e,t){return t==="-webkit-"&&(e.value=e.value.replace(/\s*(right|left)\s*/i,"")),super.set(e,t)}};Gu.names=["text-emphasis-position"];P0.exports=Gu});var q0=x((hL,D0)=>{u();var mR=j(),Qu=class extends mR{set(e,t){return e.prop==="text-decoration-skip-ink"&&e.value==="auto"?(e.prop=t+"text-decoration-skip",e.value="ink",e):super.set(e,t)}};Qu.names=["text-decoration-skip-ink","text-decoration-skip"];D0.exports=Qu});var F0=x((mL,B0)=>{u();"use strict";B0.exports={wrap:$0,limit:L0,validate:M0,test:Yu,curry:gR,name:N0};function $0(r,e,t){var i=e-r;return((t-r)%i+i)%i+r}function L0(r,e,t){return Math.max(r,Math.min(e,t))}function M0(r,e,t,i,n){if(!Yu(r,e,t,i,n))throw new Error(t+" is outside of range ["+r+","+e+")");return t}function Yu(r,e,t,i,n){return!(te||n&&t===e||i&&t===r)}function N0(r,e,t,i){return(t?"(":"[")+r+","+e+(i?")":"]")}function gR(r,e,t,i){var n=N0.bind(null,r,e,t,i);return{wrap:$0.bind(null,r,e),limit:L0.bind(null,r,e),validate:function(s){return M0(r,e,s,t,i)},test:function(s){return Yu(r,e,s,t,i)},toString:n,name:n}}});var U0=x((gL,z0)=>{u();var Ku=Ms(),yR=F0(),bR=xr(),wR=He(),vR=_e(),j0=/top|left|right|bottom/gi,wt=class extends wR{replace(e,t){let i=Ku(e);for(let n of i.nodes)if(n.type==="function"&&n.value===this.name)if(n.nodes=this.newDirection(n.nodes),n.nodes=this.normalize(n.nodes),t==="-webkit- old"){if(!this.oldWebkit(n))return!1}else n.nodes=this.convertDirection(n.nodes),n.value=t+n.value;return i.toString()}replaceFirst(e,...t){return t.map(n=>n===" "?{type:"space",value:n}:{type:"word",value:n}).concat(e.slice(1))}normalizeUnit(e,t){return`${parseFloat(e)/t*360}deg`}normalize(e){if(!e[0])return e;if(/-?\d+(.\d+)?grad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,400);else if(/-?\d+(.\d+)?rad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,2*Math.PI);else if(/-?\d+(.\d+)?turn/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,1);else if(e[0].value.includes("deg")){let t=parseFloat(e[0].value);t=yR.wrap(0,360,t),e[0].value=`${t}deg`}return e[0].value==="0deg"?e=this.replaceFirst(e,"to"," ","top"):e[0].value==="90deg"?e=this.replaceFirst(e,"to"," ","right"):e[0].value==="180deg"?e=this.replaceFirst(e,"to"," ","bottom"):e[0].value==="270deg"&&(e=this.replaceFirst(e,"to"," ","left")),e}newDirection(e){if(e[0].value==="to"||(j0.lastIndex=0,!j0.test(e[0].value)))return e;e.unshift({type:"word",value:"to"},{type:"space",value:" "});for(let t=2;t0&&(e[0].value==="to"?this.fixDirection(e):e[0].value.includes("deg")?this.fixAngle(e):this.isRadial(e)&&this.fixRadial(e)),e}fixDirection(e){e.splice(0,2);for(let t of e){if(t.type==="div")break;t.type==="word"&&(t.value=this.revertDirection(t.value))}}fixAngle(e){let t=e[0].value;t=parseFloat(t),t=Math.abs(450-t)%360,t=this.roundFloat(t,3),e[0].value=`${t}deg`}fixRadial(e){let t=[],i=[],n,s,a,o,l;for(o=0;o{u();var xR=xr(),kR=He();function V0(r){return new RegExp(`(^|[\\s,(])(${r}($|[\\s),]))`,"gi")}var Xu=class extends kR{regexp(){return this.regexpCache||(this.regexpCache=V0(this.name)),this.regexpCache}isStretch(){return this.name==="stretch"||this.name==="fill"||this.name==="fill-available"}replace(e,t){return t==="-moz-"&&this.isStretch()?e.replace(this.regexp(),"$1-moz-available$3"):t==="-webkit-"&&this.isStretch()?e.replace(this.regexp(),"$1-webkit-fill-available$3"):super.replace(e,t)}old(e){let t=e+this.name;return this.isStretch()&&(e==="-moz-"?t="-moz-available":e==="-webkit-"&&(t="-webkit-fill-available")),new xR(this.name,t,t,V0(t))}add(e,t){if(!(e.prop.includes("grid")&&t!=="-webkit-"))return super.add(e,t)}};Xu.names=["max-content","min-content","fit-content","fill","fill-available","stretch"];H0.exports=Xu});var Y0=x((bL,Q0)=>{u();var G0=xr(),SR=He(),Zu=class extends SR{replace(e,t){return t==="-webkit-"?e.replace(this.regexp(),"$1-webkit-optimize-contrast"):t==="-moz-"?e.replace(this.regexp(),"$1-moz-crisp-edges"):super.replace(e,t)}old(e){return e==="-webkit-"?new G0(this.name,"-webkit-optimize-contrast"):e==="-moz-"?new G0(this.name,"-moz-crisp-edges"):super.old(e)}};Zu.names=["pixelated"];Q0.exports=Zu});var X0=x((wL,K0)=>{u();var AR=He(),Ju=class extends AR{replace(e,t){let i=super.replace(e,t);return t==="-webkit-"&&(i=i.replace(/("[^"]+"|'[^']+')(\s+\d+\w)/gi,"url($1)$2")),i}};Ju.names=["image-set"];K0.exports=Ju});var J0=x((vL,Z0)=>{u();var CR=$e().list,_R=He(),ef=class extends _R{replace(e,t){return CR.space(e).map(i=>{if(i.slice(0,+this.name.length+1)!==this.name+"(")return i;let n=i.lastIndexOf(")"),s=i.slice(n+1),a=i.slice(this.name.length+1,n);if(t==="-webkit-"){let o=a.match(/\d*.?\d+%?/);o?(a=a.slice(o[0].length).trim(),a+=`, ${o[0]}`):a+=", 0.5"}return t+this.name+"("+a+")"+s}).join(" ")}};ef.names=["cross-fade"];Z0.exports=ef});var tv=x((xL,ev)=>{u();var ER=Pe(),OR=xr(),TR=He(),tf=class extends TR{constructor(e,t){super(e,t);e==="display-flex"&&(this.name="flex")}check(e){return e.prop==="display"&&e.value===this.name}prefixed(e){let t,i;return[t,e]=ER(e),t===2009?this.name==="flex"?i="box":i="inline-box":t===2012?this.name==="flex"?i="flexbox":i="inline-flexbox":t==="final"&&(i=this.name),e+i}replace(e,t){return this.prefixed(t)}old(e){let t=this.prefixed(e);if(!!t)return new OR(this.name,t)}};tf.names=["display-flex","inline-flex"];ev.exports=tf});var iv=x((kL,rv)=>{u();var RR=He(),rf=class extends RR{constructor(e,t){super(e,t);e==="display-grid"&&(this.name="grid")}check(e){return e.prop==="display"&&e.value===this.name}};rf.names=["display-grid","inline-grid"];rv.exports=rf});var sv=x((SL,nv)=>{u();var PR=He(),nf=class extends PR{constructor(e,t){super(e,t);e==="filter-function"&&(this.name="filter")}};nf.names=["filter","filter-function"];nv.exports=nf});var uv=x((AL,lv)=>{u();var av=Ni(),z=j(),ov=zy(),IR=ab(),DR=ou(),qR=Cb(),sf=Mt(),Pr=kr(),$R=Db(),ut=He(),Ir=_e(),LR=$b(),MR=Mb(),NR=Bb(),BR=jb(),FR=Wb(),jR=Yb(),zR=Xb(),UR=Jb(),VR=tw(),HR=iw(),WR=sw(),GR=ow(),QR=uw(),YR=cw(),KR=dw(),XR=gw(),ZR=bw(),JR=xw(),e5=Sw(),t5=Cw(),r5=Ow(),i5=Rw(),n5=Dw(),s5=$w(),a5=Mw(),o5=Bw(),l5=jw(),u5=Vw(),f5=Ww(),c5=Qw(),p5=Kw(),d5=Zw(),h5=e0(),m5=r0(),g5=s0(),y5=o0(),b5=u0(),w5=c0(),v5=d0(),x5=g0(),k5=b0(),S5=v0(),A5=S0(),C5=C0(),_5=E0(),E5=R0(),O5=I0(),T5=q0(),R5=U0(),P5=W0(),I5=Y0(),D5=X0(),q5=J0(),$5=tv(),L5=iv(),M5=sv();Pr.hack(LR);Pr.hack(MR);Pr.hack(NR);Pr.hack(BR);z.hack(FR);z.hack(jR);z.hack(zR);z.hack(UR);z.hack(VR);z.hack(HR);z.hack(WR);z.hack(GR);z.hack(QR);z.hack(YR);z.hack(KR);z.hack(XR);z.hack(ZR);z.hack(JR);z.hack(e5);z.hack(t5);z.hack(r5);z.hack(i5);z.hack(n5);z.hack(s5);z.hack(a5);z.hack(o5);z.hack(l5);z.hack(u5);z.hack(f5);z.hack(c5);z.hack(p5);z.hack(d5);z.hack(h5);z.hack(m5);z.hack(g5);z.hack(y5);z.hack(b5);z.hack(w5);z.hack(v5);z.hack(x5);z.hack(k5);z.hack(S5);z.hack(A5);z.hack(C5);z.hack(_5);z.hack(E5);z.hack(O5);z.hack(T5);ut.hack(R5);ut.hack(P5);ut.hack(I5);ut.hack(D5);ut.hack(q5);ut.hack($5);ut.hack(L5);ut.hack(M5);var af=new Map,Fi=class{constructor(e,t,i={}){this.data=e,this.browsers=t,this.options=i,[this.add,this.remove]=this.preprocess(this.select(this.data)),this.transition=new IR(this),this.processor=new DR(this)}cleaner(){if(this.cleanerCache)return this.cleanerCache;if(this.browsers.selected.length){let e=new sf(this.browsers.data,[]);this.cleanerCache=new Fi(this.data,e,this.options)}else return this;return this.cleanerCache}select(e){let t={add:{},remove:{}};for(let i in e){let n=e[i],s=n.browsers.map(l=>{let c=l.split(" ");return{browser:`${c[0]} ${c[1]}`,note:c[2]}}),a=s.filter(l=>l.note).map(l=>`${this.browsers.prefix(l.browser)} ${l.note}`);a=Ir.uniq(a),s=s.filter(l=>this.browsers.isSelected(l.browser)).map(l=>{let c=this.browsers.prefix(l.browser);return l.note?`${c} ${l.note}`:c}),s=this.sort(Ir.uniq(s)),this.options.flexbox==="no-2009"&&(s=s.filter(l=>!l.includes("2009")));let o=n.browsers.map(l=>this.browsers.prefix(l));n.mistakes&&(o=o.concat(n.mistakes)),o=o.concat(a),o=Ir.uniq(o),s.length?(t.add[i]=s,s.length!s.includes(l)))):t.remove[i]=o}return t}sort(e){return e.sort((t,i)=>{let n=Ir.removeNote(t).length,s=Ir.removeNote(i).length;return n===s?i.length-t.length:s-n})}preprocess(e){let t={selectors:[],"@supports":new qR(Fi,this)};for(let n in e.add){let s=e.add[n];if(n==="@keyframes"||n==="@viewport")t[n]=new $R(n,s,this);else if(n==="@resolution")t[n]=new ov(n,s,this);else if(this.data[n].selector)t.selectors.push(Pr.load(n,s,this));else{let a=this.data[n].props;if(a){let o=ut.load(n,s,this);for(let l of a)t[l]||(t[l]={values:[]}),t[l].values.push(o)}else{let o=t[n]&&t[n].values||[];t[n]=z.load(n,s,this),t[n].values=o}}}let i={selectors:[]};for(let n in e.remove){let s=e.remove[n];if(this.data[n].selector){let a=Pr.load(n,s);for(let o of s)i.selectors.push(a.old(o))}else if(n==="@keyframes"||n==="@viewport")for(let a of s){let o=`@${a}${n.slice(1)}`;i[o]={remove:!0}}else if(n==="@resolution")i[n]=new ov(n,s,this);else{let a=this.data[n].props;if(a){let o=ut.load(n,[],this);for(let l of s){let c=o.old(l);if(c)for(let f of a)i[f]||(i[f]={}),i[f].values||(i[f].values=[]),i[f].values.push(c)}}else for(let o of s){let l=this.decl(n).old(n,o);if(n==="align-self"){let c=t[n]&&t[n].prefixes;if(c){if(o==="-webkit- 2009"&&c.includes("-webkit-"))continue;if(o==="-webkit-"&&c.includes("-webkit- 2009"))continue}}for(let c of l)i[c]||(i[c]={}),i[c].remove=!0}}}return[t,i]}decl(e){return af.has(e)||af.set(e,z.load(e)),af.get(e)}unprefixed(e){let t=this.normalize(av.unprefixed(e));return t==="flex-direction"&&(t="flex-flow"),t}normalize(e){return this.decl(e).normalize(e)}prefixed(e,t){return e=av.unprefixed(e),this.decl(e).prefixed(e,t)}values(e,t){let i=this[e],n=i["*"]&&i["*"].values,s=i[t]&&i[t].values;return n&&s?Ir.uniq(n.concat(s)):n||s||[]}group(e){let t=e.parent,i=t.index(e),{length:n}=t.nodes,s=this.unprefixed(e.prop),a=(o,l)=>{for(i+=o;i>=0&&i{u();fv.exports={"backdrop-filter":{feature:"css-backdrop-filter",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},element:{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-element-function",browsers:["firefox 114"]},"user-select":{mistakes:["-khtml-"],feature:"user-select-none",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"background-clip":{feature:"background-clip-text",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},hyphens:{feature:"css-hyphens",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},fill:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"fill-available":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},stretch:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"fit-content":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"text-decoration-style":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-color":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-line":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip-ink":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-size-adjust":{feature:"text-size-adjust",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"mask-clip":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-composite":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-image":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-origin":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-source":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},mask:{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-position":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-size":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-outset":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-width":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-slice":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"clip-path":{feature:"css-clip-path",browsers:["samsung 21"]},"box-decoration-break":{feature:"css-boxdecorationbreak",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","opera 99","safari 16.5","samsung 21"]},appearance:{feature:"css-appearance",browsers:["samsung 21"]},"image-set":{props:["background","background-image","border-image","cursor","mask","mask-image","list-style","list-style-image","content"],feature:"css-image-set",browsers:["and_uc 15.5","chrome 109","samsung 21"]},"cross-fade":{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-cross-fade",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},isolate:{props:["unicode-bidi"],feature:"css-unicode-bidi",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"color-adjust":{feature:"css-color-adjust",browsers:["chrome 109","chrome 113","chrome 114","edge 114","opera 99"]}}});var dv=x((_L,pv)=>{u();pv.exports={}});var yv=x((EL,gv)=>{u();var N5=Yl(),{agents:B5}=(Ps(),Rs),of=Oy(),F5=Mt(),j5=uv(),z5=cv(),U5=dv(),hv={browsers:B5,prefixes:z5},mv=` + Replace Autoprefixer \`browsers\` option to Browserslist config. + Use \`browserslist\` key in \`package.json\` or \`.browserslistrc\` file. + + Using \`browsers\` option can cause errors. Browserslist config can + be used for Babel, Autoprefixer, postcss-normalize and other tools. + + If you really need to use option, rename it to \`overrideBrowserslist\`. + + Learn more at: + https://github.com/browserslist/browserslist#readme + https://twitter.com/browserslist + +`;function V5(r){return Object.prototype.toString.apply(r)==="[object Object]"}var lf=new Map;function H5(r,e){e.browsers.selected.length!==0&&(e.add.selectors.length>0||Object.keys(e.add).length>2||r.warn(`Autoprefixer target browsers do not need any prefixes.You do not need Autoprefixer anymore. +Check your Browserslist config to be sure that your targets are set up correctly. + + Learn more at: + https://github.com/postcss/autoprefixer#readme + https://github.com/browserslist/browserslist#readme + +`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})(); +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! https://mths.be/cssesc v3.0.0 by @mathias */ From 3e1cafa41e87d3bbc2ec778468fedec76aceb9c4 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 23 Mar 2026 12:15:21 +0200 Subject: [PATCH 15/48] fix: remove tailwind.min.js from tracked files (downloaded at build time) --- auth/oidc/static/tailwind.min.js | 83 -------------------------------- 1 file changed, 83 deletions(-) delete mode 100644 auth/oidc/static/tailwind.min.js diff --git a/auth/oidc/static/tailwind.min.js b/auth/oidc/static/tailwind.min.js deleted file mode 100644 index 573c16591..000000000 --- a/auth/oidc/static/tailwind.min.js +++ /dev/null @@ -1,83 +0,0 @@ -(()=>{var qv=Object.create;var Hi=Object.defineProperty;var $v=Object.getOwnPropertyDescriptor;var Lv=Object.getOwnPropertyNames;var Mv=Object.getPrototypeOf,Nv=Object.prototype.hasOwnProperty;var df=r=>Hi(r,"__esModule",{value:!0});var hf=r=>{if(typeof require!="undefined")return require(r);throw new Error('Dynamic require of "'+r+'" is not supported')};var P=(r,e)=>()=>(r&&(e=r(r=0)),e);var x=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),Ge=(r,e)=>{df(r);for(var t in e)Hi(r,t,{get:e[t],enumerable:!0})},Bv=(r,e,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Lv(e))!Nv.call(r,i)&&i!=="default"&&Hi(r,i,{get:()=>e[i],enumerable:!(t=$v(e,i))||t.enumerable});return r},pe=r=>Bv(df(Hi(r!=null?qv(Mv(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var m,u=P(()=>{m={platform:"",env:{},versions:{node:"14.17.6"}}});var Fv,be,ft=P(()=>{u();Fv=0,be={readFileSync:r=>self[r]||"",statSync:()=>({mtimeMs:Fv++}),promises:{readFile:r=>Promise.resolve(self[r]||"")}}});var Fs=x((oP,gf)=>{u();"use strict";var mf=class{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");if(typeof e.maxAge=="number"&&e.maxAge===0)throw new TypeError("`maxAge` must be a number greater than 0");this.maxSize=e.maxSize,this.maxAge=e.maxAge||1/0,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_emitEvictions(e){if(typeof this.onEviction=="function")for(let[t,i]of e)this.onEviction(t,i.value)}_deleteIfExpired(e,t){return typeof t.expiry=="number"&&t.expiry<=Date.now()?(typeof this.onEviction=="function"&&this.onEviction(e,t.value),this.delete(e)):!1}_getOrDeleteIfExpired(e,t){if(this._deleteIfExpired(e,t)===!1)return t.value}_getItemValue(e,t){return t.expiry?this._getOrDeleteIfExpired(e,t):t.value}_peek(e,t){let i=t.get(e);return this._getItemValue(e,i)}_set(e,t){this.cache.set(e,t),this._size++,this._size>=this.maxSize&&(this._size=0,this._emitEvictions(this.oldCache),this.oldCache=this.cache,this.cache=new Map)}_moveToRecent(e,t){this.oldCache.delete(e),this._set(e,t)}*_entriesAscending(){for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield e)}for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield e)}}get(e){if(this.cache.has(e)){let t=this.cache.get(e);return this._getItemValue(e,t)}if(this.oldCache.has(e)){let t=this.oldCache.get(e);if(this._deleteIfExpired(e,t)===!1)return this._moveToRecent(e,t),t.value}}set(e,t,{maxAge:i=this.maxAge===1/0?void 0:Date.now()+this.maxAge}={}){this.cache.has(e)?this.cache.set(e,{value:t,maxAge:i}):this._set(e,{value:t,expiry:i})}has(e){return this.cache.has(e)?!this._deleteIfExpired(e,this.cache.get(e)):this.oldCache.has(e)?!this._deleteIfExpired(e,this.oldCache.get(e)):!1}peek(e){if(this.cache.has(e))return this._peek(e,this.cache);if(this.oldCache.has(e))return this._peek(e,this.oldCache)}delete(e){let t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}resize(e){if(!(e&&e>0))throw new TypeError("`maxSize` must be a number greater than 0");let t=[...this._entriesAscending()],i=t.length-e;i<0?(this.cache=new Map(t),this.oldCache=new Map,this._size=t.length):(i>0&&this._emitEvictions(t.slice(0,i)),this.oldCache=new Map(t.slice(i)),this.cache=new Map,this._size=0),this.maxSize=e}*keys(){for(let[e]of this)yield e}*values(){for(let[,e]of this)yield e}*[Symbol.iterator](){for(let e of this.cache){let[t,i]=e;this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}for(let e of this.oldCache){let[t,i]=e;this.cache.has(t)||this._deleteIfExpired(t,i)===!1&&(yield[t,i.value])}}*entriesDescending(){let e=[...this.cache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,s]=i;this._deleteIfExpired(n,s)===!1&&(yield[n,s.value])}e=[...this.oldCache];for(let t=e.length-1;t>=0;--t){let i=e[t],[n,s]=i;this.cache.has(n)||this._deleteIfExpired(n,s)===!1&&(yield[n,s.value])}}*entriesAscending(){for(let[e,t]of this._entriesAscending())yield[e,t.value]}get size(){if(!this._size)return this.oldCache.size;let e=0;for(let t of this.oldCache.keys())this.cache.has(t)||e++;return Math.min(this._size+e,this.maxSize)}};gf.exports=mf});var yf,bf=P(()=>{u();yf=r=>r&&r._hash});function Wi(r){return yf(r,{ignoreUnknown:!0})}var wf=P(()=>{u();bf()});function xt(r){if(r=`${r}`,r==="0")return"0";if(/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(r))return r.replace(/^[+-]?/,t=>t==="-"?"":"-");let e=["var","calc","min","max","clamp"];for(let t of e)if(r.includes(`${t}(`))return`calc(${r} * -1)`}var Gi=P(()=>{u()});var vf,xf=P(()=>{u();vf=["preflight","container","accessibility","pointerEvents","visibility","position","inset","isolation","zIndex","order","gridColumn","gridColumnStart","gridColumnEnd","gridRow","gridRowStart","gridRowEnd","float","clear","margin","boxSizing","lineClamp","display","aspectRatio","size","height","maxHeight","minHeight","width","minWidth","maxWidth","flex","flexShrink","flexGrow","flexBasis","tableLayout","captionSide","borderCollapse","borderSpacing","transformOrigin","translate","rotate","skew","scale","transform","animation","cursor","touchAction","userSelect","resize","scrollSnapType","scrollSnapAlign","scrollSnapStop","scrollMargin","scrollPadding","listStylePosition","listStyleType","listStyleImage","appearance","columns","breakBefore","breakInside","breakAfter","gridAutoColumns","gridAutoFlow","gridAutoRows","gridTemplateColumns","gridTemplateRows","flexDirection","flexWrap","placeContent","placeItems","alignContent","alignItems","justifyContent","justifyItems","gap","space","divideWidth","divideStyle","divideColor","divideOpacity","placeSelf","alignSelf","justifySelf","overflow","overscrollBehavior","scrollBehavior","textOverflow","hyphens","whitespace","textWrap","wordBreak","borderRadius","borderWidth","borderStyle","borderColor","borderOpacity","backgroundColor","backgroundOpacity","backgroundImage","gradientColorStops","boxDecorationBreak","backgroundSize","backgroundAttachment","backgroundClip","backgroundPosition","backgroundRepeat","backgroundOrigin","fill","stroke","strokeWidth","objectFit","objectPosition","padding","textAlign","textIndent","verticalAlign","fontFamily","fontSize","fontWeight","textTransform","fontStyle","fontVariantNumeric","lineHeight","letterSpacing","textColor","textOpacity","textDecoration","textDecorationColor","textDecorationStyle","textDecorationThickness","textUnderlineOffset","fontSmoothing","placeholderColor","placeholderOpacity","caretColor","accentColor","opacity","backgroundBlendMode","mixBlendMode","boxShadow","boxShadowColor","outlineStyle","outlineWidth","outlineOffset","outlineColor","ringWidth","ringColor","ringOpacity","ringOffsetWidth","ringOffsetColor","blur","brightness","contrast","dropShadow","grayscale","hueRotate","invert","saturate","sepia","filter","backdropBlur","backdropBrightness","backdropContrast","backdropGrayscale","backdropHueRotate","backdropInvert","backdropOpacity","backdropSaturate","backdropSepia","backdropFilter","transitionProperty","transitionDelay","transitionDuration","transitionTimingFunction","willChange","contain","content","forcedColorAdjust"]});function kf(r,e){return r===void 0?e:Array.isArray(r)?r:[...new Set(e.filter(i=>r!==!1&&r[i]!==!1).concat(Object.keys(r).filter(i=>r[i]!==!1)))]}var Sf=P(()=>{u()});var Af={};Ge(Af,{default:()=>Qe});var Qe,Qi=P(()=>{u();Qe=new Proxy({},{get:()=>String})});function js(r,e,t){typeof m!="undefined"&&m.env.JEST_WORKER_ID||t&&Cf.has(t)||(t&&Cf.add(t),console.warn(""),e.forEach(i=>console.warn(r,"-",i)))}function zs(r){return Qe.dim(r)}var Cf,G,Be=P(()=>{u();Qi();Cf=new Set;G={info(r,e){js(Qe.bold(Qe.cyan("info")),...Array.isArray(r)?[r]:[e,r])},warn(r,e){["content-problems"].includes(r)||js(Qe.bold(Qe.yellow("warn")),...Array.isArray(r)?[r]:[e,r])},risk(r,e){js(Qe.bold(Qe.magenta("risk")),...Array.isArray(r)?[r]:[e,r])}}});var _f={};Ge(_f,{default:()=>Us});function qr({version:r,from:e,to:t}){G.warn(`${e}-color-renamed`,[`As of Tailwind CSS ${r}, \`${e}\` has been renamed to \`${t}\`.`,"Update your configuration file to silence this warning."])}var Us,Vs=P(()=>{u();Be();Us={inherit:"inherit",current:"currentColor",transparent:"transparent",black:"#000",white:"#fff",slate:{50:"#f8fafc",100:"#f1f5f9",200:"#e2e8f0",300:"#cbd5e1",400:"#94a3b8",500:"#64748b",600:"#475569",700:"#334155",800:"#1e293b",900:"#0f172a",950:"#020617"},gray:{50:"#f9fafb",100:"#f3f4f6",200:"#e5e7eb",300:"#d1d5db",400:"#9ca3af",500:"#6b7280",600:"#4b5563",700:"#374151",800:"#1f2937",900:"#111827",950:"#030712"},zinc:{50:"#fafafa",100:"#f4f4f5",200:"#e4e4e7",300:"#d4d4d8",400:"#a1a1aa",500:"#71717a",600:"#52525b",700:"#3f3f46",800:"#27272a",900:"#18181b",950:"#09090b"},neutral:{50:"#fafafa",100:"#f5f5f5",200:"#e5e5e5",300:"#d4d4d4",400:"#a3a3a3",500:"#737373",600:"#525252",700:"#404040",800:"#262626",900:"#171717",950:"#0a0a0a"},stone:{50:"#fafaf9",100:"#f5f5f4",200:"#e7e5e4",300:"#d6d3d1",400:"#a8a29e",500:"#78716c",600:"#57534e",700:"#44403c",800:"#292524",900:"#1c1917",950:"#0c0a09"},red:{50:"#fef2f2",100:"#fee2e2",200:"#fecaca",300:"#fca5a5",400:"#f87171",500:"#ef4444",600:"#dc2626",700:"#b91c1c",800:"#991b1b",900:"#7f1d1d",950:"#450a0a"},orange:{50:"#fff7ed",100:"#ffedd5",200:"#fed7aa",300:"#fdba74",400:"#fb923c",500:"#f97316",600:"#ea580c",700:"#c2410c",800:"#9a3412",900:"#7c2d12",950:"#431407"},amber:{50:"#fffbeb",100:"#fef3c7",200:"#fde68a",300:"#fcd34d",400:"#fbbf24",500:"#f59e0b",600:"#d97706",700:"#b45309",800:"#92400e",900:"#78350f",950:"#451a03"},yellow:{50:"#fefce8",100:"#fef9c3",200:"#fef08a",300:"#fde047",400:"#facc15",500:"#eab308",600:"#ca8a04",700:"#a16207",800:"#854d0e",900:"#713f12",950:"#422006"},lime:{50:"#f7fee7",100:"#ecfccb",200:"#d9f99d",300:"#bef264",400:"#a3e635",500:"#84cc16",600:"#65a30d",700:"#4d7c0f",800:"#3f6212",900:"#365314",950:"#1a2e05"},green:{50:"#f0fdf4",100:"#dcfce7",200:"#bbf7d0",300:"#86efac",400:"#4ade80",500:"#22c55e",600:"#16a34a",700:"#15803d",800:"#166534",900:"#14532d",950:"#052e16"},emerald:{50:"#ecfdf5",100:"#d1fae5",200:"#a7f3d0",300:"#6ee7b7",400:"#34d399",500:"#10b981",600:"#059669",700:"#047857",800:"#065f46",900:"#064e3b",950:"#022c22"},teal:{50:"#f0fdfa",100:"#ccfbf1",200:"#99f6e4",300:"#5eead4",400:"#2dd4bf",500:"#14b8a6",600:"#0d9488",700:"#0f766e",800:"#115e59",900:"#134e4a",950:"#042f2e"},cyan:{50:"#ecfeff",100:"#cffafe",200:"#a5f3fc",300:"#67e8f9",400:"#22d3ee",500:"#06b6d4",600:"#0891b2",700:"#0e7490",800:"#155e75",900:"#164e63",950:"#083344"},sky:{50:"#f0f9ff",100:"#e0f2fe",200:"#bae6fd",300:"#7dd3fc",400:"#38bdf8",500:"#0ea5e9",600:"#0284c7",700:"#0369a1",800:"#075985",900:"#0c4a6e",950:"#082f49"},blue:{50:"#eff6ff",100:"#dbeafe",200:"#bfdbfe",300:"#93c5fd",400:"#60a5fa",500:"#3b82f6",600:"#2563eb",700:"#1d4ed8",800:"#1e40af",900:"#1e3a8a",950:"#172554"},indigo:{50:"#eef2ff",100:"#e0e7ff",200:"#c7d2fe",300:"#a5b4fc",400:"#818cf8",500:"#6366f1",600:"#4f46e5",700:"#4338ca",800:"#3730a3",900:"#312e81",950:"#1e1b4b"},violet:{50:"#f5f3ff",100:"#ede9fe",200:"#ddd6fe",300:"#c4b5fd",400:"#a78bfa",500:"#8b5cf6",600:"#7c3aed",700:"#6d28d9",800:"#5b21b6",900:"#4c1d95",950:"#2e1065"},purple:{50:"#faf5ff",100:"#f3e8ff",200:"#e9d5ff",300:"#d8b4fe",400:"#c084fc",500:"#a855f7",600:"#9333ea",700:"#7e22ce",800:"#6b21a8",900:"#581c87",950:"#3b0764"},fuchsia:{50:"#fdf4ff",100:"#fae8ff",200:"#f5d0fe",300:"#f0abfc",400:"#e879f9",500:"#d946ef",600:"#c026d3",700:"#a21caf",800:"#86198f",900:"#701a75",950:"#4a044e"},pink:{50:"#fdf2f8",100:"#fce7f3",200:"#fbcfe8",300:"#f9a8d4",400:"#f472b6",500:"#ec4899",600:"#db2777",700:"#be185d",800:"#9d174d",900:"#831843",950:"#500724"},rose:{50:"#fff1f2",100:"#ffe4e6",200:"#fecdd3",300:"#fda4af",400:"#fb7185",500:"#f43f5e",600:"#e11d48",700:"#be123c",800:"#9f1239",900:"#881337",950:"#4c0519"},get lightBlue(){return qr({version:"v2.2",from:"lightBlue",to:"sky"}),this.sky},get warmGray(){return qr({version:"v3.0",from:"warmGray",to:"stone"}),this.stone},get trueGray(){return qr({version:"v3.0",from:"trueGray",to:"neutral"}),this.neutral},get coolGray(){return qr({version:"v3.0",from:"coolGray",to:"gray"}),this.gray},get blueGray(){return qr({version:"v3.0",from:"blueGray",to:"slate"}),this.slate}}});function Hs(r,...e){for(let t of e){for(let i in t)r?.hasOwnProperty?.(i)||(r[i]=t[i]);for(let i of Object.getOwnPropertySymbols(t))r?.hasOwnProperty?.(i)||(r[i]=t[i])}return r}var Ef=P(()=>{u()});function kt(r){if(Array.isArray(r))return r;let e=r.split("[").length-1,t=r.split("]").length-1;if(e!==t)throw new Error(`Path is invalid. Has unbalanced brackets: ${r}`);return r.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)}var Yi=P(()=>{u()});function we(r,e){return Ki.future.includes(e)?r.future==="all"||(r?.future?.[e]??Of[e]??!1):Ki.experimental.includes(e)?r.experimental==="all"||(r?.experimental?.[e]??Of[e]??!1):!1}function Tf(r){return r.experimental==="all"?Ki.experimental:Object.keys(r?.experimental??{}).filter(e=>Ki.experimental.includes(e)&&r.experimental[e])}function Rf(r){if(m.env.JEST_WORKER_ID===void 0&&Tf(r).length>0){let e=Tf(r).map(t=>Qe.yellow(t)).join(", ");G.warn("experimental-flags-enabled",[`You have enabled experimental features: ${e}`,"Experimental features in Tailwind CSS are not covered by semver, may introduce breaking changes, and can change at any time."])}}var Of,Ki,ct=P(()=>{u();Qi();Be();Of={optimizeUniversalDefaults:!1,generalizedModifiers:!0,disableColorOpacityUtilitiesByDefault:!1,relativeContentPathsByDefault:!1},Ki={future:["hoverOnlyWhenSupported","respectDefaultRingColorOpacity","disableColorOpacityUtilitiesByDefault","relativeContentPathsByDefault"],experimental:["optimizeUniversalDefaults","generalizedModifiers"]}});function Pf(r){(()=>{if(r.purge||!r.content||!Array.isArray(r.content)&&!(typeof r.content=="object"&&r.content!==null))return!1;if(Array.isArray(r.content))return r.content.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string"));if(typeof r.content=="object"&&r.content!==null){if(Object.keys(r.content).some(t=>!["files","relative","extract","transform"].includes(t)))return!1;if(Array.isArray(r.content.files)){if(!r.content.files.every(t=>typeof t=="string"?!0:!(typeof t?.raw!="string"||t?.extension&&typeof t?.extension!="string")))return!1;if(typeof r.content.extract=="object"){for(let t of Object.values(r.content.extract))if(typeof t!="function")return!1}else if(!(r.content.extract===void 0||typeof r.content.extract=="function"))return!1;if(typeof r.content.transform=="object"){for(let t of Object.values(r.content.transform))if(typeof t!="function")return!1}else if(!(r.content.transform===void 0||typeof r.content.transform=="function"))return!1;if(typeof r.content.relative!="boolean"&&typeof r.content.relative!="undefined")return!1}return!0}return!1})()||G.warn("purge-deprecation",["The `purge`/`content` options have changed in Tailwind CSS v3.0.","Update your configuration file to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#configure-content-sources"]),r.safelist=(()=>{let{content:t,purge:i,safelist:n}=r;return Array.isArray(n)?n:Array.isArray(t?.safelist)?t.safelist:Array.isArray(i?.safelist)?i.safelist:Array.isArray(i?.options?.safelist)?i.options.safelist:[]})(),r.blocklist=(()=>{let{blocklist:t}=r;if(Array.isArray(t)){if(t.every(i=>typeof i=="string"))return t;G.warn("blocklist-invalid",["The `blocklist` option must be an array of strings.","https://tailwindcss.com/docs/content-configuration#discarding-classes"])}return[]})(),typeof r.prefix=="function"?(G.warn("prefix-function",["As of Tailwind CSS v3.0, `prefix` cannot be a function.","Update `prefix` in your configuration to be a string to eliminate this warning.","https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function"]),r.prefix=""):r.prefix=r.prefix??"",r.content={relative:(()=>{let{content:t}=r;return t?.relative?t.relative:we(r,"relativeContentPathsByDefault")})(),files:(()=>{let{content:t,purge:i}=r;return Array.isArray(i)?i:Array.isArray(i?.content)?i.content:Array.isArray(t)?t:Array.isArray(t?.content)?t.content:Array.isArray(t?.files)?t.files:[]})(),extract:(()=>{let t=(()=>r.purge?.extract?r.purge.extract:r.content?.extract?r.content.extract:r.purge?.extract?.DEFAULT?r.purge.extract.DEFAULT:r.content?.extract?.DEFAULT?r.content.extract.DEFAULT:r.purge?.options?.extractors?r.purge.options.extractors:r.content?.options?.extractors?r.content.options.extractors:{})(),i={},n=(()=>{if(r.purge?.options?.defaultExtractor)return r.purge.options.defaultExtractor;if(r.content?.options?.defaultExtractor)return r.content.options.defaultExtractor})();if(n!==void 0&&(i.DEFAULT=n),typeof t=="function")i.DEFAULT=t;else if(Array.isArray(t))for(let{extensions:s,extractor:a}of t??[])for(let o of s)i[o]=a;else typeof t=="object"&&t!==null&&Object.assign(i,t);return i})(),transform:(()=>{let t=(()=>r.purge?.transform?r.purge.transform:r.content?.transform?r.content.transform:r.purge?.transform?.DEFAULT?r.purge.transform.DEFAULT:r.content?.transform?.DEFAULT?r.content.transform.DEFAULT:{})(),i={};return typeof t=="function"?i.DEFAULT=t:typeof t=="object"&&t!==null&&Object.assign(i,t),i})()};for(let t of r.content.files)if(typeof t=="string"&&/{([^,]*?)}/g.test(t)){G.warn("invalid-glob-braces",[`The glob pattern ${zs(t)} in your Tailwind CSS configuration is invalid.`,`Update it to ${zs(t.replace(/{([^,]*?)}/g,"$1"))} to silence this warning.`]);break}return r}var If=P(()=>{u();ct();Be()});function ke(r){if(Object.prototype.toString.call(r)!=="[object Object]")return!1;let e=Object.getPrototypeOf(r);return e===null||Object.getPrototypeOf(e)===null}var Kt=P(()=>{u()});function St(r){return Array.isArray(r)?r.map(e=>St(e)):typeof r=="object"&&r!==null?Object.fromEntries(Object.entries(r).map(([e,t])=>[e,St(t)])):r}var Xi=P(()=>{u()});function jt(r){return r.replace(/\\,/g,"\\2c ")}var Zi=P(()=>{u()});var Ws,Df=P(()=>{u();Ws={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});function $r(r,{loose:e=!1}={}){if(typeof r!="string")return null;if(r=r.trim(),r==="transparent")return{mode:"rgb",color:["0","0","0"],alpha:"0"};if(r in Ws)return{mode:"rgb",color:Ws[r].map(s=>s.toString())};let t=r.replace(zv,(s,a,o,l,c)=>["#",a,a,o,o,l,l,c?c+c:""].join("")).match(jv);if(t!==null)return{mode:"rgb",color:[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)].map(s=>s.toString()),alpha:t[4]?(parseInt(t[4],16)/255).toString():void 0};let i=r.match(Uv)??r.match(Vv);if(i===null)return null;let n=[i[2],i[3],i[4]].filter(Boolean).map(s=>s.toString());return n.length===2&&n[0].startsWith("var(")?{mode:i[1],color:[n[0]],alpha:n[1]}:!e&&n.length!==3||n.length<3&&!n.some(s=>/^var\(.*?\)$/.test(s))?null:{mode:i[1],color:n,alpha:i[5]?.toString?.()}}function Gs({mode:r,color:e,alpha:t}){let i=t!==void 0;return r==="rgba"||r==="hsla"?`${r}(${e.join(", ")}${i?`, ${t}`:""})`:`${r}(${e.join(" ")}${i?` / ${t}`:""})`}var jv,zv,At,Ji,qf,Ct,Uv,Vv,Qs=P(()=>{u();Df();jv=/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i,zv=/^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i,At=/(?:\d+|\d*\.\d+)%?/,Ji=/(?:\s*,\s*|\s+)/,qf=/\s*[,/]\s*/,Ct=/var\(--(?:[^ )]*?)(?:,(?:[^ )]*?|var\(--[^ )]*?\)))?\)/,Uv=new RegExp(`^(rgba?)\\(\\s*(${At.source}|${Ct.source})(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${qf.source}(${At.source}|${Ct.source}))?\\s*\\)$`),Vv=new RegExp(`^(hsla?)\\(\\s*((?:${At.source})(?:deg|rad|grad|turn)?|${Ct.source})(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${Ji.source}(${At.source}|${Ct.source}))?(?:${qf.source}(${At.source}|${Ct.source}))?\\s*\\)$`)});function Je(r,e,t){if(typeof r=="function")return r({opacityValue:e});let i=$r(r,{loose:!0});return i===null?t:Gs({...i,alpha:e})}function Ae({color:r,property:e,variable:t}){let i=[].concat(e);if(typeof r=="function")return{[t]:"1",...Object.fromEntries(i.map(s=>[s,r({opacityVariable:t,opacityValue:`var(${t}, 1)`})]))};let n=$r(r);return n===null?Object.fromEntries(i.map(s=>[s,r])):n.alpha!==void 0?Object.fromEntries(i.map(s=>[s,r])):{[t]:"1",...Object.fromEntries(i.map(s=>[s,Gs({...n,alpha:`var(${t}, 1)`})]))}}var Lr=P(()=>{u();Qs()});function ve(r,e){let t=[],i=[],n=0,s=!1;for(let a=0;a{u()});function en(r){return ve(r,",").map(t=>{let i=t.trim(),n={raw:i},s=i.split(Wv),a=new Set;for(let o of s)$f.lastIndex=0,!a.has("KEYWORD")&&Hv.has(o)?(n.keyword=o,a.add("KEYWORD")):$f.test(o)?a.has("X")?a.has("Y")?a.has("BLUR")?a.has("SPREAD")||(n.spread=o,a.add("SPREAD")):(n.blur=o,a.add("BLUR")):(n.y=o,a.add("Y")):(n.x=o,a.add("X")):n.color?(n.unknown||(n.unknown=[]),n.unknown.push(o)):n.color=o;return n.valid=n.x!==void 0&&n.y!==void 0,n})}function Lf(r){return r.map(e=>e.valid?[e.keyword,e.x,e.y,e.blur,e.spread,e.color].filter(Boolean).join(" "):e.raw).join(", ")}var Hv,Wv,$f,Ys=P(()=>{u();zt();Hv=new Set(["inset","inherit","initial","revert","unset"]),Wv=/\ +(?![^(]*\))/g,$f=/^-?(\d+|\.\d+)(.*?)$/g});function Ks(r){return Gv.some(e=>new RegExp(`^${e}\\(.*\\)`).test(r))}function K(r,e=null,t=!0){let i=e&&Qv.has(e.property);return r.startsWith("--")&&!i?`var(${r})`:r.includes("url(")?r.split(/(url\(.*?\))/g).filter(Boolean).map(n=>/^url\(.*?\)$/.test(n)?n:K(n,e,!1)).join(""):(r=r.replace(/([^\\])_+/g,(n,s)=>s+" ".repeat(n.length-1)).replace(/^_/g," ").replace(/\\_/g,"_"),t&&(r=r.trim()),r=Yv(r),r)}function Ye(r){return r.includes("=")&&(r=r.replace(/(=.*)/g,(e,t)=>{if(t[1]==="'"||t[1]==='"')return t;if(t.length>2){let i=t[t.length-1];if(t[t.length-2]===" "&&(i==="i"||i==="I"||i==="s"||i==="S"))return`="${t.slice(1,-2)}" ${t[t.length-1]}`}return`="${t.slice(1)}"`})),r}function Yv(r){let e=["theme"],t=["min-content","max-content","fit-content","safe-area-inset-top","safe-area-inset-right","safe-area-inset-bottom","safe-area-inset-left","titlebar-area-x","titlebar-area-y","titlebar-area-width","titlebar-area-height","keyboard-inset-top","keyboard-inset-right","keyboard-inset-bottom","keyboard-inset-left","keyboard-inset-width","keyboard-inset-height","radial-gradient","linear-gradient","conic-gradient","repeating-radial-gradient","repeating-linear-gradient","repeating-conic-gradient","anchor-size"];return r.replace(/(calc|min|max|clamp)\(.+\)/g,i=>{let n="";function s(){let a=n.trimEnd();return a[a.length-1]}for(let a=0;ai[a+p]===d)},l=function(f){let d=1/0;for(let h of f){let b=i.indexOf(h,a);b!==-1&&bo(f))){let f=t.find(d=>o(d));n+=f,a+=f.length-1}else e.some(f=>o(f))?n+=l([")"]):o("[")?n+=l(["]"]):["+","-","*","/"].includes(c)&&!["(","+","-","*","/",","].includes(s())?n+=` ${c} `:n+=c}return n.replace(/\s+/g," ")})}function Xs(r){return r.startsWith("url(")}function Zs(r){return!isNaN(Number(r))||Ks(r)}function Mr(r){return r.endsWith("%")&&Zs(r.slice(0,-1))||Ks(r)}function Nr(r){return r==="0"||new RegExp(`^[+-]?[0-9]*.?[0-9]+(?:[eE][+-]?[0-9]+)?${Xv}$`).test(r)||Ks(r)}function Mf(r){return Zv.has(r)}function Nf(r){let e=en(K(r));for(let t of e)if(!t.valid)return!1;return!0}function Bf(r){let e=0;return ve(r,"_").every(i=>(i=K(i),i.startsWith("var(")?!0:$r(i,{loose:!0})!==null?(e++,!0):!1))?e>0:!1}function Ff(r){let e=0;return ve(r,",").every(i=>(i=K(i),i.startsWith("var(")?!0:Xs(i)||ex(i)||["element(","image(","cross-fade(","image-set("].some(n=>i.startsWith(n))?(e++,!0):!1))?e>0:!1}function ex(r){r=K(r);for(let e of Jv)if(r.startsWith(`${e}(`))return!0;return!1}function jf(r){let e=0;return ve(r,"_").every(i=>(i=K(i),i.startsWith("var(")?!0:tx.has(i)||Nr(i)||Mr(i)?(e++,!0):!1))?e>0:!1}function zf(r){let e=0;return ve(r,",").every(i=>(i=K(i),i.startsWith("var(")?!0:i.includes(" ")&&!/(['"])([^"']+)\1/g.test(i)||/^\d/g.test(i)?!1:(e++,!0)))?e>0:!1}function Uf(r){return rx.has(r)}function Vf(r){return ix.has(r)}function Hf(r){return nx.has(r)}var Gv,Qv,Kv,Xv,Zv,Jv,tx,rx,ix,nx,Br=P(()=>{u();Qs();Ys();zt();Gv=["min","max","clamp","calc"];Qv=new Set(["scroll-timeline-name","timeline-scope","view-timeline-name","font-palette","anchor-name","anchor-scope","position-anchor","position-try-options","scroll-timeline","animation-timeline","view-timeline","position-try"]);Kv=["cm","mm","Q","in","pc","pt","px","em","ex","ch","rem","lh","rlh","vw","vh","vmin","vmax","vb","vi","svw","svh","lvw","lvh","dvw","dvh","cqw","cqh","cqi","cqb","cqmin","cqmax"],Xv=`(?:${Kv.join("|")})`;Zv=new Set(["thin","medium","thick"]);Jv=new Set(["conic-gradient","linear-gradient","radial-gradient","repeating-conic-gradient","repeating-linear-gradient","repeating-radial-gradient"]);tx=new Set(["center","top","right","bottom","left"]);rx=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-serif","ui-sans-serif","ui-monospace","ui-rounded","math","emoji","fangsong"]);ix=new Set(["xx-small","x-small","small","medium","large","x-large","xx-large","xxx-large"]);nx=new Set(["larger","smaller"])});function Wf(r){let e=["cover","contain"];return ve(r,",").every(t=>{let i=ve(t,"_").filter(Boolean);return i.length===1&&e.includes(i[0])?!0:i.length!==1&&i.length!==2?!1:i.every(n=>Nr(n)||Mr(n)||n==="auto")})}var Gf=P(()=>{u();Br();zt()});function Qf(r,e){r.walkClasses(t=>{t.value=e(t.value),t.raws&&t.raws.value&&(t.raws.value=jt(t.raws.value))})}function Yf(r,e){if(!_t(r))return;let t=r.slice(1,-1);if(!!e(t))return K(t)}function sx(r,e={},t){let i=e[r];if(i!==void 0)return xt(i);if(_t(r)){let n=Yf(r,t);return n===void 0?void 0:xt(n)}}function tn(r,e={},{validate:t=()=>!0}={}){let i=e.values?.[r];return i!==void 0?i:e.supportsNegativeValues&&r.startsWith("-")?sx(r.slice(1),e.values,t):Yf(r,t)}function _t(r){return r.startsWith("[")&&r.endsWith("]")}function Kf(r){let e=r.lastIndexOf("/"),t=r.lastIndexOf("[",e),i=r.indexOf("]",e);return r[e-1]==="]"||r[e+1]==="["||t!==-1&&i!==-1&&t")){let e=r;return({opacityValue:t=1})=>e.replace(//g,t)}return r}function Xf(r){return K(r.slice(1,-1))}function ax(r,e={},{tailwindConfig:t={}}={}){if(e.values?.[r]!==void 0)return Xt(e.values?.[r]);let[i,n]=Kf(r);if(n!==void 0){let s=e.values?.[i]??(_t(i)?i.slice(1,-1):void 0);return s===void 0?void 0:(s=Xt(s),_t(n)?Je(s,Xf(n)):t.theme?.opacity?.[n]===void 0?void 0:Je(s,t.theme.opacity[n]))}return tn(r,e,{validate:Bf})}function ox(r,e={}){return e.values?.[r]}function qe(r){return(e,t)=>tn(e,t,{validate:r})}function lx(r,e){let t=r.indexOf(e);return t===-1?[void 0,r]:[r.slice(0,t),r.slice(t+1)]}function ea(r,e,t,i){if(t.values&&e in t.values)for(let{type:s}of r??[]){let a=Js[s](e,t,{tailwindConfig:i});if(a!==void 0)return[a,s,null]}if(_t(e)){let s=e.slice(1,-1),[a,o]=lx(s,":");if(!/^[\w-_]+$/g.test(a))o=s;else if(a!==void 0&&!Zf.includes(a))return[];if(o.length>0&&Zf.includes(a))return[tn(`[${o}]`,t),a,null]}let n=ta(r,e,t,i);for(let s of n)return s;return[]}function*ta(r,e,t,i){let n=we(i,"generalizedModifiers"),[s,a]=Kf(e);if(n&&t.modifiers!=null&&(t.modifiers==="any"||typeof t.modifiers=="object"&&(a&&_t(a)||a in t.modifiers))||(s=e,a=void 0),a!==void 0&&s===""&&(s="DEFAULT"),a!==void 0&&typeof t.modifiers=="object"){let l=t.modifiers?.[a]??null;l!==null?a=l:_t(a)&&(a=Xf(a))}for(let{type:l}of r??[]){let c=Js[l](s,t,{tailwindConfig:i});c!==void 0&&(yield[c,l,a??null])}}var Js,Zf,Fr=P(()=>{u();Zi();Lr();Br();Gi();Gf();ct();Js={any:tn,color:ax,url:qe(Xs),image:qe(Ff),length:qe(Nr),percentage:qe(Mr),position:qe(jf),lookup:ox,"generic-name":qe(Uf),"family-name":qe(zf),number:qe(Zs),"line-width":qe(Mf),"absolute-size":qe(Vf),"relative-size":qe(Hf),shadow:qe(Nf),size:qe(Wf)},Zf=Object.keys(Js)});function X(r){return typeof r=="function"?r({}):r}var ra=P(()=>{u()});function Zt(r){return typeof r=="function"}function jr(r,...e){let t=e.pop();for(let i of e)for(let n in i){let s=t(r[n],i[n]);s===void 0?ke(r[n])&&ke(i[n])?r[n]=jr({},r[n],i[n],t):r[n]=i[n]:r[n]=s}return r}function ux(r,...e){return Zt(r)?r(...e):r}function fx(r){return r.reduce((e,{extend:t})=>jr(e,t,(i,n)=>i===void 0?[n]:Array.isArray(i)?[n,...i]:[n,i]),{})}function cx(r){return{...r.reduce((e,t)=>Hs(e,t),{}),extend:fx(r)}}function Jf(r,e){if(Array.isArray(r)&&ke(r[0]))return r.concat(e);if(Array.isArray(e)&&ke(e[0])&&ke(r))return[r,...e];if(Array.isArray(e))return e}function px({extend:r,...e}){return jr(e,r,(t,i)=>!Zt(t)&&!i.some(Zt)?jr({},t,...i,Jf):(n,s)=>jr({},...[t,...i].map(a=>ux(a,n,s)),Jf))}function*dx(r){let e=kt(r);if(e.length===0||(yield e,Array.isArray(r)))return;let t=/^(.*?)\s*\/\s*([^/]+)$/,i=r.match(t);if(i!==null){let[,n,s]=i,a=kt(n);a.alpha=s,yield a}}function hx(r){let e=(t,i)=>{for(let n of dx(t)){let s=0,a=r;for(;a!=null&&s(t[i]=Zt(r[i])?r[i](e,ia):r[i],t),{})}function ec(r){let e=[];return r.forEach(t=>{e=[...e,t];let i=t?.plugins??[];i.length!==0&&i.forEach(n=>{n.__isOptionsFunction&&(n=n()),e=[...e,...ec([n?.config??{}])]})}),e}function mx(r){return[...r].reduceRight((t,i)=>Zt(i)?i({corePlugins:t}):kf(i,t),vf)}function gx(r){return[...r].reduceRight((t,i)=>[...t,...i],[])}function na(r){let e=[...ec(r),{prefix:"",important:!1,separator:":"}];return Pf(Hs({theme:hx(px(cx(e.map(t=>t?.theme??{})))),corePlugins:mx(e.map(t=>t.corePlugins)),plugins:gx(r.map(t=>t?.plugins??[]))},...e))}var ia,tc=P(()=>{u();Gi();xf();Sf();Vs();Ef();Yi();If();Kt();Xi();Fr();Lr();ra();ia={colors:Us,negative(r){return Object.keys(r).filter(e=>r[e]!=="0").reduce((e,t)=>{let i=xt(r[t]);return i!==void 0&&(e[`-${t}`]=i),e},{})},breakpoints(r){return Object.keys(r).filter(e=>typeof r[e]=="string").reduce((e,t)=>({...e,[`screen-${t}`]:r[t]}),{})}}});var rn=x((f3,rc)=>{u();rc.exports={content:[],presets:[],darkMode:"media",theme:{accentColor:({theme:r})=>({...r("colors"),auto:"auto"}),animation:{none:"none",spin:"spin 1s linear infinite",ping:"ping 1s cubic-bezier(0, 0, 0.2, 1) infinite",pulse:"pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",bounce:"bounce 1s infinite"},aria:{busy:'busy="true"',checked:'checked="true"',disabled:'disabled="true"',expanded:'expanded="true"',hidden:'hidden="true"',pressed:'pressed="true"',readonly:'readonly="true"',required:'required="true"',selected:'selected="true"'},aspectRatio:{auto:"auto",square:"1 / 1",video:"16 / 9"},backdropBlur:({theme:r})=>r("blur"),backdropBrightness:({theme:r})=>r("brightness"),backdropContrast:({theme:r})=>r("contrast"),backdropGrayscale:({theme:r})=>r("grayscale"),backdropHueRotate:({theme:r})=>r("hueRotate"),backdropInvert:({theme:r})=>r("invert"),backdropOpacity:({theme:r})=>r("opacity"),backdropSaturate:({theme:r})=>r("saturate"),backdropSepia:({theme:r})=>r("sepia"),backgroundColor:({theme:r})=>r("colors"),backgroundImage:{none:"none","gradient-to-t":"linear-gradient(to top, var(--tw-gradient-stops))","gradient-to-tr":"linear-gradient(to top right, var(--tw-gradient-stops))","gradient-to-r":"linear-gradient(to right, var(--tw-gradient-stops))","gradient-to-br":"linear-gradient(to bottom right, var(--tw-gradient-stops))","gradient-to-b":"linear-gradient(to bottom, var(--tw-gradient-stops))","gradient-to-bl":"linear-gradient(to bottom left, var(--tw-gradient-stops))","gradient-to-l":"linear-gradient(to left, var(--tw-gradient-stops))","gradient-to-tl":"linear-gradient(to top left, var(--tw-gradient-stops))"},backgroundOpacity:({theme:r})=>r("opacity"),backgroundPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},backgroundSize:{auto:"auto",cover:"cover",contain:"contain"},blur:{0:"0",none:"",sm:"4px",DEFAULT:"8px",md:"12px",lg:"16px",xl:"24px","2xl":"40px","3xl":"64px"},borderColor:({theme:r})=>({...r("colors"),DEFAULT:r("colors.gray.200","currentColor")}),borderOpacity:({theme:r})=>r("opacity"),borderRadius:{none:"0px",sm:"0.125rem",DEFAULT:"0.25rem",md:"0.375rem",lg:"0.5rem",xl:"0.75rem","2xl":"1rem","3xl":"1.5rem",full:"9999px"},borderSpacing:({theme:r})=>({...r("spacing")}),borderWidth:{DEFAULT:"1px",0:"0px",2:"2px",4:"4px",8:"8px"},boxShadow:{sm:"0 1px 2px 0 rgb(0 0 0 / 0.05)",DEFAULT:"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",md:"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",lg:"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",xl:"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)","2xl":"0 25px 50px -12px rgb(0 0 0 / 0.25)",inner:"inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",none:"none"},boxShadowColor:({theme:r})=>r("colors"),brightness:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5",200:"2"},caretColor:({theme:r})=>r("colors"),colors:({colors:r})=>({inherit:r.inherit,current:r.current,transparent:r.transparent,black:r.black,white:r.white,slate:r.slate,gray:r.gray,zinc:r.zinc,neutral:r.neutral,stone:r.stone,red:r.red,orange:r.orange,amber:r.amber,yellow:r.yellow,lime:r.lime,green:r.green,emerald:r.emerald,teal:r.teal,cyan:r.cyan,sky:r.sky,blue:r.blue,indigo:r.indigo,violet:r.violet,purple:r.purple,fuchsia:r.fuchsia,pink:r.pink,rose:r.rose}),columns:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12","3xs":"16rem","2xs":"18rem",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem"},container:{},content:{none:"none"},contrast:{0:"0",50:".5",75:".75",100:"1",125:"1.25",150:"1.5",200:"2"},cursor:{auto:"auto",default:"default",pointer:"pointer",wait:"wait",text:"text",move:"move",help:"help","not-allowed":"not-allowed",none:"none","context-menu":"context-menu",progress:"progress",cell:"cell",crosshair:"crosshair","vertical-text":"vertical-text",alias:"alias",copy:"copy","no-drop":"no-drop",grab:"grab",grabbing:"grabbing","all-scroll":"all-scroll","col-resize":"col-resize","row-resize":"row-resize","n-resize":"n-resize","e-resize":"e-resize","s-resize":"s-resize","w-resize":"w-resize","ne-resize":"ne-resize","nw-resize":"nw-resize","se-resize":"se-resize","sw-resize":"sw-resize","ew-resize":"ew-resize","ns-resize":"ns-resize","nesw-resize":"nesw-resize","nwse-resize":"nwse-resize","zoom-in":"zoom-in","zoom-out":"zoom-out"},divideColor:({theme:r})=>r("borderColor"),divideOpacity:({theme:r})=>r("borderOpacity"),divideWidth:({theme:r})=>r("borderWidth"),dropShadow:{sm:"0 1px 1px rgb(0 0 0 / 0.05)",DEFAULT:["0 1px 2px rgb(0 0 0 / 0.1)","0 1px 1px rgb(0 0 0 / 0.06)"],md:["0 4px 3px rgb(0 0 0 / 0.07)","0 2px 2px rgb(0 0 0 / 0.06)"],lg:["0 10px 8px rgb(0 0 0 / 0.04)","0 4px 3px rgb(0 0 0 / 0.1)"],xl:["0 20px 13px rgb(0 0 0 / 0.03)","0 8px 5px rgb(0 0 0 / 0.08)"],"2xl":"0 25px 25px rgb(0 0 0 / 0.15)",none:"0 0 #0000"},fill:({theme:r})=>({none:"none",...r("colors")}),flex:{1:"1 1 0%",auto:"1 1 auto",initial:"0 1 auto",none:"none"},flexBasis:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%"}),flexGrow:{0:"0",DEFAULT:"1"},flexShrink:{0:"0",DEFAULT:"1"},fontFamily:{sans:["ui-sans-serif","system-ui","sans-serif",'"Apple Color Emoji"','"Segoe UI Emoji"','"Segoe UI Symbol"','"Noto Color Emoji"'],serif:["ui-serif","Georgia","Cambria",'"Times New Roman"',"Times","serif"],mono:["ui-monospace","SFMono-Regular","Menlo","Monaco","Consolas",'"Liberation Mono"','"Courier New"',"monospace"]},fontSize:{xs:["0.75rem",{lineHeight:"1rem"}],sm:["0.875rem",{lineHeight:"1.25rem"}],base:["1rem",{lineHeight:"1.5rem"}],lg:["1.125rem",{lineHeight:"1.75rem"}],xl:["1.25rem",{lineHeight:"1.75rem"}],"2xl":["1.5rem",{lineHeight:"2rem"}],"3xl":["1.875rem",{lineHeight:"2.25rem"}],"4xl":["2.25rem",{lineHeight:"2.5rem"}],"5xl":["3rem",{lineHeight:"1"}],"6xl":["3.75rem",{lineHeight:"1"}],"7xl":["4.5rem",{lineHeight:"1"}],"8xl":["6rem",{lineHeight:"1"}],"9xl":["8rem",{lineHeight:"1"}]},fontWeight:{thin:"100",extralight:"200",light:"300",normal:"400",medium:"500",semibold:"600",bold:"700",extrabold:"800",black:"900"},gap:({theme:r})=>r("spacing"),gradientColorStops:({theme:r})=>r("colors"),gradientColorStopPositions:{"0%":"0%","5%":"5%","10%":"10%","15%":"15%","20%":"20%","25%":"25%","30%":"30%","35%":"35%","40%":"40%","45%":"45%","50%":"50%","55%":"55%","60%":"60%","65%":"65%","70%":"70%","75%":"75%","80%":"80%","85%":"85%","90%":"90%","95%":"95%","100%":"100%"},grayscale:{0:"0",DEFAULT:"100%"},gridAutoColumns:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridAutoRows:{auto:"auto",min:"min-content",max:"max-content",fr:"minmax(0, 1fr)"},gridColumn:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridColumnEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridColumnStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRow:{auto:"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},gridRowEnd:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridRowStart:{auto:"auto",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12",13:"13"},gridTemplateColumns:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},gridTemplateRows:{none:"none",subgrid:"subgrid",1:"repeat(1, minmax(0, 1fr))",2:"repeat(2, minmax(0, 1fr))",3:"repeat(3, minmax(0, 1fr))",4:"repeat(4, minmax(0, 1fr))",5:"repeat(5, minmax(0, 1fr))",6:"repeat(6, minmax(0, 1fr))",7:"repeat(7, minmax(0, 1fr))",8:"repeat(8, minmax(0, 1fr))",9:"repeat(9, minmax(0, 1fr))",10:"repeat(10, minmax(0, 1fr))",11:"repeat(11, minmax(0, 1fr))",12:"repeat(12, minmax(0, 1fr))"},height:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),hueRotate:{0:"0deg",15:"15deg",30:"30deg",60:"60deg",90:"90deg",180:"180deg"},inset:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),invert:{0:"0",DEFAULT:"100%"},keyframes:{spin:{to:{transform:"rotate(360deg)"}},ping:{"75%, 100%":{transform:"scale(2)",opacity:"0"}},pulse:{"50%":{opacity:".5"}},bounce:{"0%, 100%":{transform:"translateY(-25%)",animationTimingFunction:"cubic-bezier(0.8,0,1,1)"},"50%":{transform:"none",animationTimingFunction:"cubic-bezier(0,0,0.2,1)"}}},letterSpacing:{tighter:"-0.05em",tight:"-0.025em",normal:"0em",wide:"0.025em",wider:"0.05em",widest:"0.1em"},lineHeight:{none:"1",tight:"1.25",snug:"1.375",normal:"1.5",relaxed:"1.625",loose:"2",3:".75rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem"},listStyleType:{none:"none",disc:"disc",decimal:"decimal"},listStyleImage:{none:"none"},margin:({theme:r})=>({auto:"auto",...r("spacing")}),lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"},maxHeight:({theme:r})=>({...r("spacing"),none:"none",full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),maxWidth:({theme:r,breakpoints:e})=>({...r("spacing"),none:"none",xs:"20rem",sm:"24rem",md:"28rem",lg:"32rem",xl:"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem",full:"100%",min:"min-content",max:"max-content",fit:"fit-content",prose:"65ch",...e(r("screens"))}),minHeight:({theme:r})=>({...r("spacing"),full:"100%",screen:"100vh",svh:"100svh",lvh:"100lvh",dvh:"100dvh",min:"min-content",max:"max-content",fit:"fit-content"}),minWidth:({theme:r})=>({...r("spacing"),full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),objectPosition:{bottom:"bottom",center:"center",left:"left","left-bottom":"left bottom","left-top":"left top",right:"right","right-bottom":"right bottom","right-top":"right top",top:"top"},opacity:{0:"0",5:"0.05",10:"0.1",15:"0.15",20:"0.2",25:"0.25",30:"0.3",35:"0.35",40:"0.4",45:"0.45",50:"0.5",55:"0.55",60:"0.6",65:"0.65",70:"0.7",75:"0.75",80:"0.8",85:"0.85",90:"0.9",95:"0.95",100:"1"},order:{first:"-9999",last:"9999",none:"0",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",10:"10",11:"11",12:"12"},outlineColor:({theme:r})=>r("colors"),outlineOffset:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},outlineWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},padding:({theme:r})=>r("spacing"),placeholderColor:({theme:r})=>r("colors"),placeholderOpacity:({theme:r})=>r("opacity"),ringColor:({theme:r})=>({DEFAULT:r("colors.blue.500","#3b82f6"),...r("colors")}),ringOffsetColor:({theme:r})=>r("colors"),ringOffsetWidth:{0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},ringOpacity:({theme:r})=>({DEFAULT:"0.5",...r("opacity")}),ringWidth:{DEFAULT:"3px",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},rotate:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg",45:"45deg",90:"90deg",180:"180deg"},saturate:{0:"0",50:".5",100:"1",150:"1.5",200:"2"},scale:{0:"0",50:".5",75:".75",90:".9",95:".95",100:"1",105:"1.05",110:"1.1",125:"1.25",150:"1.5"},screens:{sm:"640px",md:"768px",lg:"1024px",xl:"1280px","2xl":"1536px"},scrollMargin:({theme:r})=>({...r("spacing")}),scrollPadding:({theme:r})=>r("spacing"),sepia:{0:"0",DEFAULT:"100%"},skew:{0:"0deg",1:"1deg",2:"2deg",3:"3deg",6:"6deg",12:"12deg"},space:({theme:r})=>({...r("spacing")}),spacing:{px:"1px",0:"0px",.5:"0.125rem",1:"0.25rem",1.5:"0.375rem",2:"0.5rem",2.5:"0.625rem",3:"0.75rem",3.5:"0.875rem",4:"1rem",5:"1.25rem",6:"1.5rem",7:"1.75rem",8:"2rem",9:"2.25rem",10:"2.5rem",11:"2.75rem",12:"3rem",14:"3.5rem",16:"4rem",20:"5rem",24:"6rem",28:"7rem",32:"8rem",36:"9rem",40:"10rem",44:"11rem",48:"12rem",52:"13rem",56:"14rem",60:"15rem",64:"16rem",72:"18rem",80:"20rem",96:"24rem"},stroke:({theme:r})=>({none:"none",...r("colors")}),strokeWidth:{0:"0",1:"1",2:"2"},supports:{},data:{},textColor:({theme:r})=>r("colors"),textDecorationColor:({theme:r})=>r("colors"),textDecorationThickness:{auto:"auto","from-font":"from-font",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},textIndent:({theme:r})=>({...r("spacing")}),textOpacity:({theme:r})=>r("opacity"),textUnderlineOffset:{auto:"auto",0:"0px",1:"1px",2:"2px",4:"4px",8:"8px"},transformOrigin:{center:"center",top:"top","top-right":"top right",right:"right","bottom-right":"bottom right",bottom:"bottom","bottom-left":"bottom left",left:"left","top-left":"top left"},transitionDelay:{0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionDuration:{DEFAULT:"150ms",0:"0s",75:"75ms",100:"100ms",150:"150ms",200:"200ms",300:"300ms",500:"500ms",700:"700ms",1e3:"1000ms"},transitionProperty:{none:"none",all:"all",DEFAULT:"color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter",colors:"color, background-color, border-color, text-decoration-color, fill, stroke",opacity:"opacity",shadow:"box-shadow",transform:"transform"},transitionTimingFunction:{DEFAULT:"cubic-bezier(0.4, 0, 0.2, 1)",linear:"linear",in:"cubic-bezier(0.4, 0, 1, 1)",out:"cubic-bezier(0, 0, 0.2, 1)","in-out":"cubic-bezier(0.4, 0, 0.2, 1)"},translate:({theme:r})=>({...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%",full:"100%"}),size:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",min:"min-content",max:"max-content",fit:"fit-content"}),width:({theme:r})=>({auto:"auto",...r("spacing"),"1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%",full:"100%",screen:"100vw",svw:"100svw",lvw:"100lvw",dvw:"100dvw",min:"min-content",max:"max-content",fit:"fit-content"}),willChange:{auto:"auto",scroll:"scroll-position",contents:"contents",transform:"transform"},zIndex:{auto:"auto",0:"0",10:"10",20:"20",30:"30",40:"40",50:"50"}},plugins:[]}});function nn(r){let e=(r?.presets??[ic.default]).slice().reverse().flatMap(n=>nn(n instanceof Function?n():n)),t={respectDefaultRingColorOpacity:{theme:{ringColor:({theme:n})=>({DEFAULT:"#3b82f67f",...n("colors")})}},disableColorOpacityUtilitiesByDefault:{corePlugins:{backgroundOpacity:!1,borderOpacity:!1,divideOpacity:!1,placeholderOpacity:!1,ringOpacity:!1,textOpacity:!1}}},i=Object.keys(t).filter(n=>we(r,n)).map(n=>t[n]);return[r,...i,...e]}var ic,nc=P(()=>{u();ic=pe(rn());ct()});var sc={};Ge(sc,{default:()=>zr});function zr(...r){let[,...e]=nn(r[0]);return na([...r,...e])}var sa=P(()=>{u();tc();nc()});var Ur={};Ge(Ur,{default:()=>me});var me,et=P(()=>{u();me={resolve:r=>r,extname:r=>"."+r.split(".").pop()}});function sn(r){return typeof r=="object"&&r!==null}function bx(r){return Object.keys(r).length===0}function ac(r){return typeof r=="string"||r instanceof String}function aa(r){return sn(r)&&r.config===void 0&&!bx(r)?null:sn(r)&&r.config!==void 0&&ac(r.config)?me.resolve(r.config):sn(r)&&r.config!==void 0&&sn(r.config)?null:ac(r)?me.resolve(r):wx()}function wx(){for(let r of yx)try{let e=me.resolve(r);return be.accessSync(e),e}catch(e){}return null}var yx,oc=P(()=>{u();ft();et();yx=["./tailwind.config.js","./tailwind.config.cjs","./tailwind.config.mjs","./tailwind.config.ts","./tailwind.config.cts","./tailwind.config.mts"]});var lc={};Ge(lc,{default:()=>oa});var oa,la=P(()=>{u();oa={parse:r=>({href:r})}});var ua=x(()=>{u()});var an=x((v3,cc)=>{u();"use strict";var uc=(Qi(),Af),fc=ua(),Jt=class extends Error{constructor(e,t,i,n,s,a){super(e);this.name="CssSyntaxError",this.reason=e,s&&(this.file=s),n&&(this.source=n),a&&(this.plugin=a),typeof t!="undefined"&&typeof i!="undefined"&&(typeof t=="number"?(this.line=t,this.column=i):(this.line=t.line,this.column=t.column,this.endLine=i.line,this.endColumn=i.column)),this.setMessage(),Error.captureStackTrace&&Error.captureStackTrace(this,Jt)}setMessage(){this.message=this.plugin?this.plugin+": ":"",this.message+=this.file?this.file:"",typeof this.line!="undefined"&&(this.message+=":"+this.line+":"+this.column),this.message+=": "+this.reason}showSourceCode(e){if(!this.source)return"";let t=this.source;e==null&&(e=uc.isColorSupported);let i=f=>f,n=f=>f,s=f=>f;if(e){let{bold:f,gray:d,red:p}=uc.createColors(!0);n=h=>f(p(h)),i=h=>d(h),fc&&(s=h=>fc(h))}let a=t.split(/\r?\n/),o=Math.max(this.line-3,0),l=Math.min(this.line+2,a.length),c=String(l).length;return a.slice(o,l).map((f,d)=>{let p=o+1+d,h=" "+(" "+p).slice(-c)+" | ";if(p===this.line){if(f.length>160){let v=20,y=Math.max(0,this.column-v),w=Math.max(this.column+v,this.endColumn+v),k=f.slice(y,w),S=i(h.replace(/\d/g," "))+f.slice(0,Math.min(this.column-1,v-1)).replace(/[^\t]/g," ");return n(">")+i(h)+s(k)+` - `+S+n("^")}let b=i(h.replace(/\d/g," "))+f.slice(0,this.column-1).replace(/[^\t]/g," ");return n(">")+i(h)+s(f)+` - `+b+n("^")}return" "+i(h)+s(f)}).join(` -`)}toString(){let e=this.showSourceCode();return e&&(e=` - -`+e+` -`),this.name+": "+this.message+e}};cc.exports=Jt;Jt.default=Jt});var fa=x((x3,dc)=>{u();"use strict";var pc={after:` -`,beforeClose:` -`,beforeComment:` -`,beforeDecl:` -`,beforeOpen:" ",beforeRule:` -`,colon:": ",commentLeft:" ",commentRight:" ",emptyBody:"",indent:" ",semicolon:!1};function vx(r){return r[0].toUpperCase()+r.slice(1)}var on=class{constructor(e){this.builder=e}atrule(e,t){let i="@"+e.name,n=e.params?this.rawValue(e,"params"):"";if(typeof e.raws.afterName!="undefined"?i+=e.raws.afterName:n&&(i+=" "),e.nodes)this.block(e,i+n);else{let s=(e.raws.between||"")+(t?";":"");this.builder(i+n+s,e)}}beforeAfter(e,t){let i;e.type==="decl"?i=this.raw(e,null,"beforeDecl"):e.type==="comment"?i=this.raw(e,null,"beforeComment"):t==="before"?i=this.raw(e,null,"beforeRule"):i=this.raw(e,null,"beforeClose");let n=e.parent,s=0;for(;n&&n.type!=="root";)s+=1,n=n.parent;if(i.includes(` -`)){let a=this.raw(e,null,"indent");if(a.length)for(let o=0;o0&&e.nodes[t].type==="comment";)t-=1;let i=this.raw(e,"semicolon");for(let n=0;n{if(n=l.raws[t],typeof n!="undefined")return!1})}return typeof n=="undefined"&&(n=pc[i]),a.rawCache[i]=n,n}rawBeforeClose(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length>0&&typeof i.raws.after!="undefined")return t=i.raws.after,t.includes(` -`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawBeforeComment(e,t){let i;return e.walkComments(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` -`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeDecl"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeDecl(e,t){let i;return e.walkDecls(n=>{if(typeof n.raws.before!="undefined")return i=n.raws.before,i.includes(` -`)&&(i=i.replace(/[^\n]+$/,"")),!1}),typeof i=="undefined"?i=this.raw(t,null,"beforeRule"):i&&(i=i.replace(/\S/g,"")),i}rawBeforeOpen(e){let t;return e.walk(i=>{if(i.type!=="decl"&&(t=i.raws.between,typeof t!="undefined"))return!1}),t}rawBeforeRule(e){let t;return e.walk(i=>{if(i.nodes&&(i.parent!==e||e.first!==i)&&typeof i.raws.before!="undefined")return t=i.raws.before,t.includes(` -`)&&(t=t.replace(/[^\n]+$/,"")),!1}),t&&(t=t.replace(/\S/g,"")),t}rawColon(e){let t;return e.walkDecls(i=>{if(typeof i.raws.between!="undefined")return t=i.raws.between.replace(/[^\s:]/g,""),!1}),t}rawEmptyBody(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length===0&&(t=i.raws.after,typeof t!="undefined"))return!1}),t}rawIndent(e){if(e.raws.indent)return e.raws.indent;let t;return e.walk(i=>{let n=i.parent;if(n&&n!==e&&n.parent&&n.parent===e&&typeof i.raws.before!="undefined"){let s=i.raws.before.split(` -`);return t=s[s.length-1],t=t.replace(/\S/g,""),!1}}),t}rawSemicolon(e){let t;return e.walk(i=>{if(i.nodes&&i.nodes.length&&i.last.type==="decl"&&(t=i.raws.semicolon,typeof t!="undefined"))return!1}),t}rawValue(e,t){let i=e[t],n=e.raws[t];return n&&n.value===i?n.raw:i}root(e){this.body(e),e.raws.after&&this.builder(e.raws.after)}rule(e){this.block(e,this.rawValue(e,"selector")),e.raws.ownSemicolon&&this.builder(e.raws.ownSemicolon,e,"end")}stringify(e,t){if(!this[e.type])throw new Error("Unknown AST node type "+e.type+". Maybe you need to change PostCSS stringifier.");this[e.type](e,t)}};dc.exports=on;on.default=on});var Vr=x((k3,hc)=>{u();"use strict";var xx=fa();function ca(r,e){new xx(e).stringify(r)}hc.exports=ca;ca.default=ca});var ln=x((S3,pa)=>{u();"use strict";pa.exports.isClean=Symbol("isClean");pa.exports.my=Symbol("my")});var Gr=x((A3,mc)=>{u();"use strict";var kx=an(),Sx=fa(),Ax=Vr(),{isClean:Hr,my:Cx}=ln();function da(r,e){let t=new r.constructor;for(let i in r){if(!Object.prototype.hasOwnProperty.call(r,i)||i==="proxyCache")continue;let n=r[i],s=typeof n;i==="parent"&&s==="object"?e&&(t[i]=e):i==="source"?t[i]=n:Array.isArray(n)?t[i]=n.map(a=>da(a,t)):(s==="object"&&n!==null&&(n=da(n)),t[i]=n)}return t}function Wr(r,e){if(e&&typeof e.offset!="undefined")return e.offset;let t=1,i=1,n=0;for(let s=0;se.root().toProxy():e[t]},set(e,t,i){return e[t]===i||(e[t]=i,(t==="prop"||t==="value"||t==="name"||t==="params"||t==="important"||t==="text")&&e.markDirty()),!0}}}markClean(){this[Hr]=!0}markDirty(){if(this[Hr]){this[Hr]=!1;let e=this;for(;e=e.parent;)e[Hr]=!1}}next(){if(!this.parent)return;let e=this.parent.index(this);return this.parent.nodes[e+1]}positionBy(e){let t=this.source.start;if(e.index)t=this.positionInside(e.index);else if(e.word){let n=this.source.input.css.slice(Wr(this.source.input.css,this.source.start),Wr(this.source.input.css,this.source.end)).indexOf(e.word);n!==-1&&(t=this.positionInside(n))}return t}positionInside(e){let t=this.source.start.column,i=this.source.start.line,n=Wr(this.source.input.css,this.source.start),s=n+e;for(let a=n;atypeof l=="object"&&l.toJSON?l.toJSON(null,t):l);else if(typeof o=="object"&&o.toJSON)i[a]=o.toJSON(null,t);else if(a==="source"){let l=t.get(o.input);l==null&&(l=s,t.set(o.input,s),s++),i[a]={end:o.end,inputId:l,start:o.start}}else i[a]=o}return n&&(i.inputs=[...t.keys()].map(a=>a.toJSON())),i}toProxy(){return this.proxyCache||(this.proxyCache=new Proxy(this,this.getProxyProcessor())),this.proxyCache}toString(e=Ax){e.stringify&&(e=e.stringify);let t="";return e(this,i=>{t+=i}),t}warn(e,t,i){let n={node:this};for(let s in i)n[s]=i[s];return e.warn(t,n)}get proxyOf(){return this}};mc.exports=un;un.default=un});var Qr=x((C3,gc)=>{u();"use strict";var _x=Gr(),fn=class extends _x{constructor(e){super(e);this.type="comment"}};gc.exports=fn;fn.default=fn});var Yr=x((_3,yc)=>{u();"use strict";var Ex=Gr(),cn=class extends Ex{constructor(e){e&&typeof e.value!="undefined"&&typeof e.value!="string"&&(e={...e,value:String(e.value)});super(e);this.type="decl"}get variable(){return this.prop.startsWith("--")||this.prop[0]==="$"}};yc.exports=cn;cn.default=cn});var Et=x((E3,_c)=>{u();"use strict";var bc=Qr(),wc=Yr(),Ox=Gr(),{isClean:vc,my:xc}=ln(),ha,kc,Sc,ma;function Ac(r){return r.map(e=>(e.nodes&&(e.nodes=Ac(e.nodes)),delete e.source,e))}function Cc(r){if(r[vc]=!1,r.proxyOf.nodes)for(let e of r.proxyOf.nodes)Cc(e)}var Fe=class extends Ox{append(...e){for(let t of e){let i=this.normalize(t,this.last);for(let n of i)this.proxyOf.nodes.push(n)}return this.markDirty(),this}cleanRaws(e){if(super.cleanRaws(e),this.nodes)for(let t of this.nodes)t.cleanRaws(e)}each(e){if(!this.proxyOf.nodes)return;let t=this.getIterator(),i,n;for(;this.indexes[t]e[t](...i.map(n=>typeof n=="function"?(s,a)=>n(s.toProxy(),a):n)):t==="every"||t==="some"?i=>e[t]((n,...s)=>i(n.toProxy(),...s)):t==="root"?()=>e.root().toProxy():t==="nodes"?e.nodes.map(i=>i.toProxy()):t==="first"||t==="last"?e[t].toProxy():e[t]:e[t]},set(e,t,i){return e[t]===i||(e[t]=i,(t==="name"||t==="params"||t==="selector")&&e.markDirty()),!0}}}index(e){return typeof e=="number"?e:(e.proxyOf&&(e=e.proxyOf),this.proxyOf.nodes.indexOf(e))}insertAfter(e,t){let i=this.index(e),n=this.normalize(t,this.proxyOf.nodes[i]).reverse();i=this.index(e);for(let a of n)this.proxyOf.nodes.splice(i+1,0,a);let s;for(let a in this.indexes)s=this.indexes[a],i(n[xc]||Fe.rebuild(n),n=n.proxyOf,n.parent&&n.parent.removeChild(n),n[vc]&&Cc(n),n.raws||(n.raws={}),typeof n.raws.before=="undefined"&&t&&typeof t.raws.before!="undefined"&&(n.raws.before=t.raws.before.replace(/\S/g,"")),n.parent=this.proxyOf,n))}prepend(...e){e=e.reverse();for(let t of e){let i=this.normalize(t,this.first,"prepend").reverse();for(let n of i)this.proxyOf.nodes.unshift(n);for(let n in this.indexes)this.indexes[n]=this.indexes[n]+i.length}return this.markDirty(),this}push(e){return e.parent=this,this.proxyOf.nodes.push(e),this}removeAll(){for(let e of this.proxyOf.nodes)e.parent=void 0;return this.proxyOf.nodes=[],this.markDirty(),this}removeChild(e){e=this.index(e),this.proxyOf.nodes[e].parent=void 0,this.proxyOf.nodes.splice(e,1);let t;for(let i in this.indexes)t=this.indexes[i],t>=e&&(this.indexes[i]=t-1);return this.markDirty(),this}replaceValues(e,t,i){return i||(i=t,t={}),this.walkDecls(n=>{t.props&&!t.props.includes(n.prop)||t.fast&&!n.value.includes(t.fast)||(n.value=n.value.replace(e,i))}),this.markDirty(),this}some(e){return this.nodes.some(e)}walk(e){return this.each((t,i)=>{let n;try{n=e(t,i)}catch(s){throw t.addToError(s)}return n!==!1&&t.walk&&(n=t.walk(e)),n})}walkAtRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="atrule"&&e.test(i.name))return t(i,n)}):this.walk((i,n)=>{if(i.type==="atrule"&&i.name===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="atrule")return t(i,n)}))}walkComments(e){return this.walk((t,i)=>{if(t.type==="comment")return e(t,i)})}walkDecls(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="decl"&&e.test(i.prop))return t(i,n)}):this.walk((i,n)=>{if(i.type==="decl"&&i.prop===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="decl")return t(i,n)}))}walkRules(e,t){return t?e instanceof RegExp?this.walk((i,n)=>{if(i.type==="rule"&&e.test(i.selector))return t(i,n)}):this.walk((i,n)=>{if(i.type==="rule"&&i.selector===e)return t(i,n)}):(t=e,this.walk((i,n)=>{if(i.type==="rule")return t(i,n)}))}get first(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[0]}get last(){if(!!this.proxyOf.nodes)return this.proxyOf.nodes[this.proxyOf.nodes.length-1]}};Fe.registerParse=r=>{kc=r};Fe.registerRule=r=>{ma=r};Fe.registerAtRule=r=>{ha=r};Fe.registerRoot=r=>{Sc=r};_c.exports=Fe;Fe.default=Fe;Fe.rebuild=r=>{r.type==="atrule"?Object.setPrototypeOf(r,ha.prototype):r.type==="rule"?Object.setPrototypeOf(r,ma.prototype):r.type==="decl"?Object.setPrototypeOf(r,wc.prototype):r.type==="comment"?Object.setPrototypeOf(r,bc.prototype):r.type==="root"&&Object.setPrototypeOf(r,Sc.prototype),r[xc]=!0,r.nodes&&r.nodes.forEach(e=>{Fe.rebuild(e)})}});var pn=x((O3,Oc)=>{u();"use strict";var Ec=Et(),Kr=class extends Ec{constructor(e){super(e);this.type="atrule"}append(...e){return this.proxyOf.nodes||(this.nodes=[]),super.append(...e)}prepend(...e){return this.proxyOf.nodes||(this.nodes=[]),super.prepend(...e)}};Oc.exports=Kr;Kr.default=Kr;Ec.registerAtRule(Kr)});var dn=x((T3,Pc)=>{u();"use strict";var Tx=Et(),Tc,Rc,er=class extends Tx{constructor(e){super({type:"document",...e});this.nodes||(this.nodes=[])}toResult(e={}){return new Tc(new Rc,this,e).stringify()}};er.registerLazyResult=r=>{Tc=r};er.registerProcessor=r=>{Rc=r};Pc.exports=er;er.default=er});var Dc=x((R3,Ic)=>{u();var Rx="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict",Px=(r,e=21)=>(t=e)=>{let i="",n=t;for(;n--;)i+=r[Math.random()*r.length|0];return i},Ix=(r=21)=>{let e="",t=r;for(;t--;)e+=Rx[Math.random()*64|0];return e};Ic.exports={nanoid:Ix,customAlphabet:Px}});var qc=x(()=>{u()});var ga=x((D3,$c)=>{u();$c.exports={}});var mn=x((q3,Bc)=>{u();"use strict";var{nanoid:Dx}=Dc(),{isAbsolute:ya,resolve:ba}=(et(),Ur),{SourceMapConsumer:qx,SourceMapGenerator:$x}=qc(),{fileURLToPath:Lc,pathToFileURL:hn}=(la(),lc),Mc=an(),Lx=ga(),wa=ua(),va=Symbol("fromOffsetCache"),Mx=Boolean(qx&&$x),Nc=Boolean(ba&&ya),Xr=class{constructor(e,t={}){if(e===null||typeof e=="undefined"||typeof e=="object"&&!e.toString)throw new Error(`PostCSS received ${e} instead of CSS string`);if(this.css=e.toString(),this.css[0]==="\uFEFF"||this.css[0]==="\uFFFE"?(this.hasBOM=!0,this.css=this.css.slice(1)):this.hasBOM=!1,t.from&&(!Nc||/^\w+:\/\//.test(t.from)||ya(t.from)?this.file=t.from:this.file=ba(t.from)),Nc&&Mx){let i=new Lx(this.css,t);if(i.text){this.map=i;let n=i.consumer().file;!this.file&&n&&(this.file=this.mapResolve(n))}}this.file||(this.id=""),this.map&&(this.map.file=this.from)}error(e,t,i,n={}){let s,a,o;if(t&&typeof t=="object"){let c=t,f=i;if(typeof c.offset=="number"){let d=this.fromOffset(c.offset);t=d.line,i=d.col}else t=c.line,i=c.column;if(typeof f.offset=="number"){let d=this.fromOffset(f.offset);a=d.line,s=d.col}else a=f.line,s=f.column}else if(!i){let c=this.fromOffset(t);t=c.line,i=c.col}let l=this.origin(t,i,a,s);return l?o=new Mc(e,l.endLine===void 0?l.line:{column:l.column,line:l.line},l.endLine===void 0?l.column:{column:l.endColumn,line:l.endLine},l.source,l.file,n.plugin):o=new Mc(e,a===void 0?t:{column:i,line:t},a===void 0?i:{column:s,line:a},this.css,this.file,n.plugin),o.input={column:i,endColumn:s,endLine:a,line:t,source:this.css},this.file&&(hn&&(o.input.url=hn(this.file).toString()),o.input.file=this.file),o}fromOffset(e){let t,i;if(this[va])i=this[va];else{let s=this.css.split(` -`);i=new Array(s.length);let a=0;for(let o=0,l=s.length;o=t)n=i.length-1;else{let s=i.length-2,a;for(;n>1),e=i[a+1])n=a+1;else{n=a;break}}return{col:e-i[n]+1,line:n+1}}mapResolve(e){return/^\w+:\/\//.test(e)?e:ba(this.map.consumer().sourceRoot||this.map.root||".",e)}origin(e,t,i,n){if(!this.map)return!1;let s=this.map.consumer(),a=s.originalPositionFor({column:t,line:e});if(!a.source)return!1;let o;typeof i=="number"&&(o=s.originalPositionFor({column:n,line:i}));let l;ya(a.source)?l=hn(a.source):l=new URL(a.source,this.map.consumer().sourceRoot||hn(this.map.mapFile));let c={column:a.column,endColumn:o&&o.column,endLine:o&&o.line,line:a.line,url:l.toString()};if(l.protocol==="file:")if(Lc)c.file=Lc(l);else throw new Error("file: protocol is not available in this PostCSS build");let f=s.sourceContentFor(a.source);return f&&(c.source=f),c}toJSON(){let e={};for(let t of["hasBOM","css","file","id"])this[t]!=null&&(e[t]=this[t]);return this.map&&(e.map={...this.map},e.map.consumerCache&&(e.map.consumerCache=void 0)),e}get from(){return this.file||this.id}};Bc.exports=Xr;Xr.default=Xr;wa&&wa.registerInput&&wa.registerInput(Xr)});var tr=x(($3,Uc)=>{u();"use strict";var Fc=Et(),jc,zc,Ut=class extends Fc{constructor(e){super(e);this.type="root",this.nodes||(this.nodes=[])}normalize(e,t,i){let n=super.normalize(e);if(t){if(i==="prepend")this.nodes.length>1?t.raws.before=this.nodes[1].raws.before:delete t.raws.before;else if(this.first!==t)for(let s of n)s.raws.before=t.raws.before}return n}removeChild(e,t){let i=this.index(e);return!t&&i===0&&this.nodes.length>1&&(this.nodes[1].raws.before=this.nodes[i].raws.before),super.removeChild(e)}toResult(e={}){return new jc(new zc,this,e).stringify()}};Ut.registerLazyResult=r=>{jc=r};Ut.registerProcessor=r=>{zc=r};Uc.exports=Ut;Ut.default=Ut;Fc.registerRoot(Ut)});var xa=x((L3,Vc)=>{u();"use strict";var Zr={comma(r){return Zr.split(r,[","],!0)},space(r){let e=[" ",` -`," "];return Zr.split(r,e)},split(r,e,t){let i=[],n="",s=!1,a=0,o=!1,l="",c=!1;for(let f of r)c?c=!1:f==="\\"?c=!0:o?f===l&&(o=!1):f==='"'||f==="'"?(o=!0,l=f):f==="("?a+=1:f===")"?a>0&&(a-=1):a===0&&e.includes(f)&&(s=!0),s?(n!==""&&i.push(n.trim()),n="",s=!1):n+=f;return(t||n!=="")&&i.push(n.trim()),i}};Vc.exports=Zr;Zr.default=Zr});var gn=x((M3,Wc)=>{u();"use strict";var Hc=Et(),Nx=xa(),Jr=class extends Hc{constructor(e){super(e);this.type="rule",this.nodes||(this.nodes=[])}get selectors(){return Nx.comma(this.selector)}set selectors(e){let t=this.selector?this.selector.match(/,\s*/):null,i=t?t[0]:","+this.raw("between","beforeOpen");this.selector=e.join(i)}};Wc.exports=Jr;Jr.default=Jr;Hc.registerRule(Jr)});var Qc=x((N3,Gc)=>{u();"use strict";var Bx=pn(),Fx=Qr(),jx=Yr(),zx=mn(),Ux=ga(),Vx=tr(),Hx=gn();function ei(r,e){if(Array.isArray(r))return r.map(n=>ei(n));let{inputs:t,...i}=r;if(t){e=[];for(let n of t){let s={...n,__proto__:zx.prototype};s.map&&(s.map={...s.map,__proto__:Ux.prototype}),e.push(s)}}if(i.nodes&&(i.nodes=r.nodes.map(n=>ei(n,e))),i.source){let{inputId:n,...s}=i.source;i.source=s,n!=null&&(i.source.input=e[n])}if(i.type==="root")return new Vx(i);if(i.type==="decl")return new jx(i);if(i.type==="rule")return new Hx(i);if(i.type==="comment")return new Fx(i);if(i.type==="atrule")return new Bx(i);throw new Error("Unknown node type: "+r.type)}Gc.exports=ei;ei.default=ei});var ka=x((B3,Yc)=>{u();Yc.exports=function(r,e){return{generate:()=>{let t="";return r(e,i=>{t+=i}),[t]}}}});var ep=x((F3,Jc)=>{u();"use strict";var Sa="'".charCodeAt(0),Kc='"'.charCodeAt(0),yn="\\".charCodeAt(0),Xc="/".charCodeAt(0),bn=` -`.charCodeAt(0),ti=" ".charCodeAt(0),wn="\f".charCodeAt(0),vn=" ".charCodeAt(0),xn="\r".charCodeAt(0),Wx="[".charCodeAt(0),Gx="]".charCodeAt(0),Qx="(".charCodeAt(0),Yx=")".charCodeAt(0),Kx="{".charCodeAt(0),Xx="}".charCodeAt(0),Zx=";".charCodeAt(0),Jx="*".charCodeAt(0),e1=":".charCodeAt(0),t1="@".charCodeAt(0),kn=/[\t\n\f\r "#'()/;[\\\]{}]/g,Sn=/[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g,r1=/.[\r\n"'(/\\]/,Zc=/[\da-f]/i;Jc.exports=function(e,t={}){let i=e.css.valueOf(),n=t.ignoreErrors,s,a,o,l,c,f,d,p,h,b,v=i.length,y=0,w=[],k=[];function S(){return y}function E(R){throw e.error("Unclosed "+R,y)}function T(){return k.length===0&&y>=v}function B(R){if(k.length)return k.pop();if(y>=v)return;let F=R?R.ignoreUnclosed:!1;switch(s=i.charCodeAt(y),s){case bn:case ti:case vn:case xn:case wn:{l=y;do l+=1,s=i.charCodeAt(l);while(s===ti||s===bn||s===vn||s===xn||s===wn);f=["space",i.slice(y,l)],y=l-1;break}case Wx:case Gx:case Kx:case Xx:case e1:case Zx:case Yx:{let Y=String.fromCharCode(s);f=[Y,Y,y];break}case Qx:{if(b=w.length?w.pop()[1]:"",h=i.charCodeAt(y+1),b==="url"&&h!==Sa&&h!==Kc&&h!==ti&&h!==bn&&h!==vn&&h!==wn&&h!==xn){l=y;do{if(d=!1,l=i.indexOf(")",l+1),l===-1)if(n||F){l=y;break}else E("bracket");for(p=l;i.charCodeAt(p-1)===yn;)p-=1,d=!d}while(d);f=["brackets",i.slice(y,l+1),y,l],y=l}else l=i.indexOf(")",y+1),a=i.slice(y,l+1),l===-1||r1.test(a)?f=["(","(",y]:(f=["brackets",a,y,l],y=l);break}case Sa:case Kc:{c=s===Sa?"'":'"',l=y;do{if(d=!1,l=i.indexOf(c,l+1),l===-1)if(n||F){l=y+1;break}else E("string");for(p=l;i.charCodeAt(p-1)===yn;)p-=1,d=!d}while(d);f=["string",i.slice(y,l+1),y,l],y=l;break}case t1:{kn.lastIndex=y+1,kn.test(i),kn.lastIndex===0?l=i.length-1:l=kn.lastIndex-2,f=["at-word",i.slice(y,l+1),y,l],y=l;break}case yn:{for(l=y,o=!0;i.charCodeAt(l+1)===yn;)l+=1,o=!o;if(s=i.charCodeAt(l+1),o&&s!==Xc&&s!==ti&&s!==bn&&s!==vn&&s!==xn&&s!==wn&&(l+=1,Zc.test(i.charAt(l)))){for(;Zc.test(i.charAt(l+1));)l+=1;i.charCodeAt(l+1)===ti&&(l+=1)}f=["word",i.slice(y,l+1),y,l],y=l;break}default:{s===Xc&&i.charCodeAt(y+1)===Jx?(l=i.indexOf("*/",y+2)+1,l===0&&(n||F?l=i.length:E("comment")),f=["comment",i.slice(y,l+1),y,l],y=l):(Sn.lastIndex=y+1,Sn.test(i),Sn.lastIndex===0?l=i.length-1:l=Sn.lastIndex-2,f=["word",i.slice(y,l+1),y,l],w.push(f),y=l);break}}return y++,f}function N(R){k.push(R)}return{back:N,endOfFile:T,nextToken:B,position:S}}});var sp=x((j3,np)=>{u();"use strict";var i1=pn(),n1=Qr(),s1=Yr(),a1=tr(),tp=gn(),o1=ep(),rp={empty:!0,space:!0};function l1(r){for(let e=r.length-1;e>=0;e--){let t=r[e],i=t[3]||t[2];if(i)return i}}var ip=class{constructor(e){this.input=e,this.root=new a1,this.current=this.root,this.spaces="",this.semicolon=!1,this.createTokenizer(),this.root.source={input:e,start:{column:1,line:1,offset:0}}}atrule(e){let t=new i1;t.name=e[1].slice(1),t.name===""&&this.unnamedAtrule(t,e),this.init(t,e[2]);let i,n,s,a=!1,o=!1,l=[],c=[];for(;!this.tokenizer.endOfFile();){if(e=this.tokenizer.nextToken(),i=e[0],i==="("||i==="["?c.push(i==="("?")":"]"):i==="{"&&c.length>0?c.push("}"):i===c[c.length-1]&&c.pop(),c.length===0)if(i===";"){t.source.end=this.getPosition(e[2]),t.source.end.offset++,this.semicolon=!0;break}else if(i==="{"){o=!0;break}else if(i==="}"){if(l.length>0){for(s=l.length-1,n=l[s];n&&n[0]==="space";)n=l[--s];n&&(t.source.end=this.getPosition(n[3]||n[2]),t.source.end.offset++)}this.end(e);break}else l.push(e);else l.push(e);if(this.tokenizer.endOfFile()){a=!0;break}}t.raws.between=this.spacesAndCommentsFromEnd(l),l.length?(t.raws.afterName=this.spacesAndCommentsFromStart(l),this.raw(t,"params",l),a&&(e=l[l.length-1],t.source.end=this.getPosition(e[3]||e[2]),t.source.end.offset++,this.spaces=t.raws.between,t.raws.between="")):(t.raws.afterName="",t.params=""),o&&(t.nodes=[],this.current=t)}checkMissedSemicolon(e){let t=this.colon(e);if(t===!1)return;let i=0,n;for(let s=t-1;s>=0&&(n=e[s],!(n[0]!=="space"&&(i+=1,i===2)));s--);throw this.input.error("Missed semicolon",n[0]==="word"?n[3]+1:n[2])}colon(e){let t=0,i,n,s;for(let[a,o]of e.entries()){if(n=o,s=n[0],s==="("&&(t+=1),s===")"&&(t-=1),t===0&&s===":")if(!i)this.doubleColon(n);else{if(i[0]==="word"&&i[1]==="progid")continue;return a}i=n}return!1}comment(e){let t=new n1;this.init(t,e[2]),t.source.end=this.getPosition(e[3]||e[2]),t.source.end.offset++;let i=e[1].slice(2,-2);if(/^\s*$/.test(i))t.text="",t.raws.left=i,t.raws.right="";else{let n=i.match(/^(\s*)([^]*\S)(\s*)$/);t.text=n[2],t.raws.left=n[1],t.raws.right=n[3]}}createTokenizer(){this.tokenizer=o1(this.input)}decl(e,t){let i=new s1;this.init(i,e[0][2]);let n=e[e.length-1];for(n[0]===";"&&(this.semicolon=!0,e.pop()),i.source.end=this.getPosition(n[3]||n[2]||l1(e)),i.source.end.offset++;e[0][0]!=="word";)e.length===1&&this.unknownWord(e),i.raws.before+=e.shift()[1];for(i.source.start=this.getPosition(e[0][2]),i.prop="";e.length;){let c=e[0][0];if(c===":"||c==="space"||c==="comment")break;i.prop+=e.shift()[1]}i.raws.between="";let s;for(;e.length;)if(s=e.shift(),s[0]===":"){i.raws.between+=s[1];break}else s[0]==="word"&&/\w/.test(s[1])&&this.unknownWord([s]),i.raws.between+=s[1];(i.prop[0]==="_"||i.prop[0]==="*")&&(i.raws.before+=i.prop[0],i.prop=i.prop.slice(1));let a=[],o;for(;e.length&&(o=e[0][0],!(o!=="space"&&o!=="comment"));)a.push(e.shift());this.precheckMissedSemicolon(e);for(let c=e.length-1;c>=0;c--){if(s=e[c],s[1].toLowerCase()==="!important"){i.important=!0;let f=this.stringFrom(e,c);f=this.spacesFromEnd(e)+f,f!==" !important"&&(i.raws.important=f);break}else if(s[1].toLowerCase()==="important"){let f=e.slice(0),d="";for(let p=c;p>0;p--){let h=f[p][0];if(d.trim().startsWith("!")&&h!=="space")break;d=f.pop()[1]+d}d.trim().startsWith("!")&&(i.important=!0,i.raws.important=d,e=f)}if(s[0]!=="space"&&s[0]!=="comment")break}e.some(c=>c[0]!=="space"&&c[0]!=="comment")&&(i.raws.between+=a.map(c=>c[1]).join(""),a=[]),this.raw(i,"value",a.concat(e),t),i.value.includes(":")&&!t&&this.checkMissedSemicolon(e)}doubleColon(e){throw this.input.error("Double colon",{offset:e[2]},{offset:e[2]+e[1].length})}emptyRule(e){let t=new tp;this.init(t,e[2]),t.selector="",t.raws.between="",this.current=t}end(e){this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.semicolon=!1,this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.spaces="",this.current.parent?(this.current.source.end=this.getPosition(e[2]),this.current.source.end.offset++,this.current=this.current.parent):this.unexpectedClose(e)}endFile(){this.current.parent&&this.unclosedBlock(),this.current.nodes&&this.current.nodes.length&&(this.current.raws.semicolon=this.semicolon),this.current.raws.after=(this.current.raws.after||"")+this.spaces,this.root.source.end=this.getPosition(this.tokenizer.position())}freeSemicolon(e){if(this.spaces+=e[1],this.current.nodes){let t=this.current.nodes[this.current.nodes.length-1];t&&t.type==="rule"&&!t.raws.ownSemicolon&&(t.raws.ownSemicolon=this.spaces,this.spaces="")}}getPosition(e){let t=this.input.fromOffset(e);return{column:t.col,line:t.line,offset:e}}init(e,t){this.current.push(e),e.source={input:this.input,start:this.getPosition(t)},e.raws.before=this.spaces,this.spaces="",e.type!=="comment"&&(this.semicolon=!1)}other(e){let t=!1,i=null,n=!1,s=null,a=[],o=e[1].startsWith("--"),l=[],c=e;for(;c;){if(i=c[0],l.push(c),i==="("||i==="[")s||(s=c),a.push(i==="("?")":"]");else if(o&&n&&i==="{")s||(s=c),a.push("}");else if(a.length===0)if(i===";")if(n){this.decl(l,o);return}else break;else if(i==="{"){this.rule(l);return}else if(i==="}"){this.tokenizer.back(l.pop()),t=!0;break}else i===":"&&(n=!0);else i===a[a.length-1]&&(a.pop(),a.length===0&&(s=null));c=this.tokenizer.nextToken()}if(this.tokenizer.endOfFile()&&(t=!0),a.length>0&&this.unclosedBracket(s),t&&n){if(!o)for(;l.length&&(c=l[l.length-1][0],!(c!=="space"&&c!=="comment"));)this.tokenizer.back(l.pop());this.decl(l,o)}else this.unknownWord(l)}parse(){let e;for(;!this.tokenizer.endOfFile();)switch(e=this.tokenizer.nextToken(),e[0]){case"space":this.spaces+=e[1];break;case";":this.freeSemicolon(e);break;case"}":this.end(e);break;case"comment":this.comment(e);break;case"at-word":this.atrule(e);break;case"{":this.emptyRule(e);break;default:this.other(e);break}this.endFile()}precheckMissedSemicolon(){}raw(e,t,i,n){let s,a,o=i.length,l="",c=!0,f,d;for(let p=0;ph+b[1],"");e.raws[t]={raw:p,value:l}}e[t]=l}rule(e){e.pop();let t=new tp;this.init(t,e[0][2]),t.raws.between=this.spacesAndCommentsFromEnd(e),this.raw(t,"selector",e),this.current=t}spacesAndCommentsFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],!(t!=="space"&&t!=="comment"));)i=e.pop()[1]+i;return i}spacesAndCommentsFromStart(e){let t,i="";for(;e.length&&(t=e[0][0],!(t!=="space"&&t!=="comment"));)i+=e.shift()[1];return i}spacesFromEnd(e){let t,i="";for(;e.length&&(t=e[e.length-1][0],t==="space");)i=e.pop()[1]+i;return i}stringFrom(e,t){let i="";for(let n=t;n{u();"use strict";var u1=Et(),f1=mn(),c1=sp();function An(r,e){let t=new f1(r,e),i=new c1(t);try{i.parse()}catch(n){throw n}return i.root}ap.exports=An;An.default=An;u1.registerParse(An)});var Aa=x((U3,op)=>{u();"use strict";var _n=class{constructor(e,t={}){if(this.type="warning",this.text=e,t.node&&t.node.source){let i=t.node.rangeBy(t);this.line=i.start.line,this.column=i.start.column,this.endLine=i.end.line,this.endColumn=i.end.column}for(let i in t)this[i]=t[i]}toString(){return this.node?this.node.error(this.text,{index:this.index,plugin:this.plugin,word:this.word}).message:this.plugin?this.plugin+": "+this.text:this.text}};op.exports=_n;_n.default=_n});var On=x((V3,lp)=>{u();"use strict";var p1=Aa(),En=class{constructor(e,t,i){this.processor=e,this.messages=[],this.root=t,this.opts=i,this.css=void 0,this.map=void 0}toString(){return this.css}warn(e,t={}){t.plugin||this.lastPlugin&&this.lastPlugin.postcssPlugin&&(t.plugin=this.lastPlugin.postcssPlugin);let i=new p1(e,t);return this.messages.push(i),i}warnings(){return this.messages.filter(e=>e.type==="warning")}get content(){return this.css}};lp.exports=En;En.default=En});var Ca=x((H3,fp)=>{u();"use strict";var up={};fp.exports=function(e){up[e]||(up[e]=!0,typeof console!="undefined"&&console.warn&&console.warn(e))}});var Oa=x((G3,hp)=>{u();"use strict";var d1=Et(),h1=dn(),m1=ka(),g1=Cn(),cp=On(),y1=tr(),b1=Vr(),{isClean:tt,my:w1}=ln(),W3=Ca(),v1={atrule:"AtRule",comment:"Comment",decl:"Declaration",document:"Document",root:"Root",rule:"Rule"},x1={AtRule:!0,AtRuleExit:!0,Comment:!0,CommentExit:!0,Declaration:!0,DeclarationExit:!0,Document:!0,DocumentExit:!0,Once:!0,OnceExit:!0,postcssPlugin:!0,prepare:!0,Root:!0,RootExit:!0,Rule:!0,RuleExit:!0},k1={Once:!0,postcssPlugin:!0,prepare:!0},rr=0;function ri(r){return typeof r=="object"&&typeof r.then=="function"}function pp(r){let e=!1,t=v1[r.type];return r.type==="decl"?e=r.prop.toLowerCase():r.type==="atrule"&&(e=r.name.toLowerCase()),e&&r.append?[t,t+"-"+e,rr,t+"Exit",t+"Exit-"+e]:e?[t,t+"-"+e,t+"Exit",t+"Exit-"+e]:r.append?[t,rr,t+"Exit"]:[t,t+"Exit"]}function dp(r){let e;return r.type==="document"?e=["Document",rr,"DocumentExit"]:r.type==="root"?e=["Root",rr,"RootExit"]:e=pp(r),{eventIndex:0,events:e,iterator:0,node:r,visitorIndex:0,visitors:[]}}function _a(r){return r[tt]=!1,r.nodes&&r.nodes.forEach(e=>_a(e)),r}var Ea={},pt=class{constructor(e,t,i){this.stringified=!1,this.processed=!1;let n;if(typeof t=="object"&&t!==null&&(t.type==="root"||t.type==="document"))n=_a(t);else if(t instanceof pt||t instanceof cp)n=_a(t.root),t.map&&(typeof i.map=="undefined"&&(i.map={}),i.map.inline||(i.map.inline=!1),i.map.prev=t.map);else{let s=g1;i.syntax&&(s=i.syntax.parse),i.parser&&(s=i.parser),s.parse&&(s=s.parse);try{n=s(t,i)}catch(a){this.processed=!0,this.error=a}n&&!n[w1]&&d1.rebuild(n)}this.result=new cp(e,n,i),this.helpers={...Ea,postcss:Ea,result:this.result},this.plugins=this.processor.plugins.map(s=>typeof s=="object"&&s.prepare?{...s,...s.prepare(this.result)}:s)}async(){return this.error?Promise.reject(this.error):this.processed?Promise.resolve(this.result):(this.processing||(this.processing=this.runAsync()),this.processing)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}getAsyncError(){throw new Error("Use process(css).then(cb) to work with async plugins")}handleError(e,t){let i=this.result.lastPlugin;try{t&&t.addToError(e),this.error=e,e.name==="CssSyntaxError"&&!e.plugin?(e.plugin=i.postcssPlugin,e.setMessage()):i.postcssVersion}catch(n){console&&console.error&&console.error(n)}return e}prepareVisitors(){this.listeners={};let e=(t,i,n)=>{this.listeners[i]||(this.listeners[i]=[]),this.listeners[i].push([t,n])};for(let t of this.plugins)if(typeof t=="object")for(let i in t){if(!x1[i]&&/^[A-Z]/.test(i))throw new Error(`Unknown event ${i} in ${t.postcssPlugin}. Try to update PostCSS (${this.processor.version} now).`);if(!k1[i])if(typeof t[i]=="object")for(let n in t[i])n==="*"?e(t,i,t[i][n]):e(t,i+"-"+n.toLowerCase(),t[i][n]);else typeof t[i]=="function"&&e(t,i,t[i])}this.hasListener=Object.keys(this.listeners).length>0}async runAsync(){this.plugin=0;for(let e=0;e0;){let i=this.visitTick(t);if(ri(i))try{await i}catch(n){let s=t[t.length-1].node;throw this.handleError(n,s)}}}if(this.listeners.OnceExit)for(let[t,i]of this.listeners.OnceExit){this.result.lastPlugin=t;try{if(e.type==="document"){let n=e.nodes.map(s=>i(s,this.helpers));await Promise.all(n)}else await i(e,this.helpers)}catch(n){throw this.handleError(n)}}}return this.processed=!0,this.stringify()}runOnRoot(e){this.result.lastPlugin=e;try{if(typeof e=="object"&&e.Once){if(this.result.root.type==="document"){let t=this.result.root.nodes.map(i=>e.Once(i,this.helpers));return ri(t[0])?Promise.all(t):t}return e.Once(this.result.root,this.helpers)}else if(typeof e=="function")return e(this.result.root,this.result)}catch(t){throw this.handleError(t)}}stringify(){if(this.error)throw this.error;if(this.stringified)return this.result;this.stringified=!0,this.sync();let e=this.result.opts,t=b1;e.syntax&&(t=e.syntax.stringify),e.stringifier&&(t=e.stringifier),t.stringify&&(t=t.stringify);let n=new m1(t,this.result.root,this.result.opts).generate();return this.result.css=n[0],this.result.map=n[1],this.result}sync(){if(this.error)throw this.error;if(this.processed)return this.result;if(this.processed=!0,this.processing)throw this.getAsyncError();for(let e of this.plugins){let t=this.runOnRoot(e);if(ri(t))throw this.getAsyncError()}if(this.prepareVisitors(),this.hasListener){let e=this.result.root;for(;!e[tt];)e[tt]=!0,this.walkSync(e);if(this.listeners.OnceExit)if(e.type==="document")for(let t of e.nodes)this.visitSync(this.listeners.OnceExit,t);else this.visitSync(this.listeners.OnceExit,e)}return this.result}then(e,t){return this.async().then(e,t)}toString(){return this.css}visitSync(e,t){for(let[i,n]of e){this.result.lastPlugin=i;let s;try{s=n(t,this.helpers)}catch(a){throw this.handleError(a,t.proxyOf)}if(t.type!=="root"&&t.type!=="document"&&!t.parent)return!0;if(ri(s))throw this.getAsyncError()}}visitTick(e){let t=e[e.length-1],{node:i,visitors:n}=t;if(i.type!=="root"&&i.type!=="document"&&!i.parent){e.pop();return}if(n.length>0&&t.visitorIndex{n[tt]||this.walkSync(n)});else{let n=this.listeners[i];if(n&&this.visitSync(n,e.toProxy()))return}}warnings(){return this.sync().warnings()}get content(){return this.stringify().content}get css(){return this.stringify().css}get map(){return this.stringify().map}get messages(){return this.sync().messages}get opts(){return this.result.opts}get processor(){return this.result.processor}get root(){return this.sync().root}get[Symbol.toStringTag](){return"LazyResult"}};pt.registerPostcss=r=>{Ea=r};hp.exports=pt;pt.default=pt;y1.registerLazyResult(pt);h1.registerLazyResult(pt)});var gp=x((Y3,mp)=>{u();"use strict";var S1=ka(),A1=Cn(),C1=On(),_1=Vr(),Q3=Ca(),Tn=class{constructor(e,t,i){t=t.toString(),this.stringified=!1,this._processor=e,this._css=t,this._opts=i,this._map=void 0;let n,s=_1;this.result=new C1(this._processor,n,this._opts),this.result.css=t;let a=this;Object.defineProperty(this.result,"root",{get(){return a.root}});let o=new S1(s,n,this._opts,t);if(o.isMap()){let[l,c]=o.generate();l&&(this.result.css=l),c&&(this.result.map=c)}else o.clearAnnotation(),this.result.css=o.css}async(){return this.error?Promise.reject(this.error):Promise.resolve(this.result)}catch(e){return this.async().catch(e)}finally(e){return this.async().then(e,e)}sync(){if(this.error)throw this.error;return this.result}then(e,t){return this.async().then(e,t)}toString(){return this._css}warnings(){return[]}get content(){return this.result.css}get css(){return this.result.css}get map(){return this.result.map}get messages(){return[]}get opts(){return this.result.opts}get processor(){return this.result.processor}get root(){if(this._root)return this._root;let e,t=A1;try{e=t(this._css,this._opts)}catch(i){this.error=i}if(this.error)throw this.error;return this._root=e,e}get[Symbol.toStringTag](){return"NoWorkResult"}};mp.exports=Tn;Tn.default=Tn});var bp=x((K3,yp)=>{u();"use strict";var E1=dn(),O1=Oa(),T1=gp(),R1=tr(),ir=class{constructor(e=[]){this.version="8.4.49",this.plugins=this.normalize(e)}normalize(e){let t=[];for(let i of e)if(i.postcss===!0?i=i():i.postcss&&(i=i.postcss),typeof i=="object"&&Array.isArray(i.plugins))t=t.concat(i.plugins);else if(typeof i=="object"&&i.postcssPlugin)t.push(i);else if(typeof i=="function")t.push(i);else if(!(typeof i=="object"&&(i.parse||i.stringify)))throw new Error(i+" is not a PostCSS plugin");return t}process(e,t={}){return!this.plugins.length&&!t.parser&&!t.stringifier&&!t.syntax?new T1(this,e,t):new O1(this,e,t)}use(e){return this.plugins=this.plugins.concat(this.normalize([e])),this}};yp.exports=ir;ir.default=ir;R1.registerProcessor(ir);E1.registerProcessor(ir)});var $e=x((X3,Cp)=>{u();"use strict";var wp=pn(),vp=Qr(),P1=Et(),I1=an(),xp=Yr(),kp=dn(),D1=Qc(),q1=mn(),$1=Oa(),L1=xa(),M1=Gr(),N1=Cn(),Ta=bp(),B1=On(),Sp=tr(),Ap=gn(),F1=Vr(),j1=Aa();function J(...r){return r.length===1&&Array.isArray(r[0])&&(r=r[0]),new Ta(r)}J.plugin=function(e,t){let i=!1;function n(...a){console&&console.warn&&!i&&(i=!0,console.warn(e+`: postcss.plugin was deprecated. Migration guide: -https://evilmartians.com/chronicles/postcss-8-plugin-migration`),m.env.LANG&&m.env.LANG.startsWith("cn")&&console.warn(e+`: \u91CC\u9762 postcss.plugin \u88AB\u5F03\u7528. \u8FC1\u79FB\u6307\u5357: -https://www.w3ctech.com/topic/2226`));let o=t(...a);return o.postcssPlugin=e,o.postcssVersion=new Ta().version,o}let s;return Object.defineProperty(n,"postcss",{get(){return s||(s=n()),s}}),n.process=function(a,o,l){return J([n(l)]).process(a,o)},n};J.stringify=F1;J.parse=N1;J.fromJSON=D1;J.list=L1;J.comment=r=>new vp(r);J.atRule=r=>new wp(r);J.decl=r=>new xp(r);J.rule=r=>new Ap(r);J.root=r=>new Sp(r);J.document=r=>new kp(r);J.CssSyntaxError=I1;J.Declaration=xp;J.Container=P1;J.Processor=Ta;J.Document=kp;J.Comment=vp;J.Warning=j1;J.AtRule=wp;J.Result=B1;J.Input=q1;J.Rule=Ap;J.Root=Sp;J.Node=M1;$1.registerPostcss(J);Cp.exports=J;J.default=J});var re,ee,Z3,J3,eI,tI,rI,iI,nI,sI,aI,oI,lI,uI,fI,cI,pI,dI,hI,mI,gI,yI,bI,wI,vI,xI,Ot=P(()=>{u();re=pe($e()),ee=re.default,Z3=re.default.stringify,J3=re.default.fromJSON,eI=re.default.plugin,tI=re.default.parse,rI=re.default.list,iI=re.default.document,nI=re.default.comment,sI=re.default.atRule,aI=re.default.rule,oI=re.default.decl,lI=re.default.root,uI=re.default.CssSyntaxError,fI=re.default.Declaration,cI=re.default.Container,pI=re.default.Processor,dI=re.default.Document,hI=re.default.Comment,mI=re.default.Warning,gI=re.default.AtRule,yI=re.default.Result,bI=re.default.Input,wI=re.default.Rule,vI=re.default.Root,xI=re.default.Node});var Ra=x((SI,_p)=>{u();_p.exports=function(r,e,t,i,n){for(e=e.split?e.split("."):e,i=0;i{u();"use strict";Rn.__esModule=!0;Rn.default=V1;function z1(r){for(var e=r.toLowerCase(),t="",i=!1,n=0;n<6&&e[n]!==void 0;n++){var s=e.charCodeAt(n),a=s>=97&&s<=102||s>=48&&s<=57;if(i=s===32,!a)break;t+=e[n]}if(t.length!==0){var o=parseInt(t,16),l=o>=55296&&o<=57343;return l||o===0||o>1114111?["\uFFFD",t.length+(i?1:0)]:[String.fromCodePoint(o),t.length+(i?1:0)]}}var U1=/\\/;function V1(r){var e=U1.test(r);if(!e)return r;for(var t="",i=0;i{u();"use strict";In.__esModule=!0;In.default=H1;function H1(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();if(!r[n])return;r=r[n]}return r}Op.exports=In.default});var Pp=x((Dn,Rp)=>{u();"use strict";Dn.__esModule=!0;Dn.default=W1;function W1(r){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i0;){var n=t.shift();r[n]||(r[n]={}),r=r[n]}}Rp.exports=Dn.default});var Dp=x((qn,Ip)=>{u();"use strict";qn.__esModule=!0;qn.default=G1;function G1(r){for(var e="",t=r.indexOf("/*"),i=0;t>=0;){e=e+r.slice(i,t);var n=r.indexOf("*/",t+2);if(n<0)return e;i=n+2,t=r.indexOf("/*",i)}return e=e+r.slice(i),e}Ip.exports=qn.default});var ii=x(rt=>{u();"use strict";rt.__esModule=!0;rt.unesc=rt.stripComments=rt.getProp=rt.ensureObject=void 0;var Q1=$n(Pn());rt.unesc=Q1.default;var Y1=$n(Tp());rt.getProp=Y1.default;var K1=$n(Pp());rt.ensureObject=K1.default;var X1=$n(Dp());rt.stripComments=X1.default;function $n(r){return r&&r.__esModule?r:{default:r}}});var dt=x((ni,Lp)=>{u();"use strict";ni.__esModule=!0;ni.default=void 0;var qp=ii();function $p(r,e){for(var t=0;ti||this.source.end.linen||this.source.end.line===i&&this.source.end.column{u();"use strict";ie.__esModule=!0;ie.UNIVERSAL=ie.TAG=ie.STRING=ie.SELECTOR=ie.ROOT=ie.PSEUDO=ie.NESTING=ie.ID=ie.COMMENT=ie.COMBINATOR=ie.CLASS=ie.ATTRIBUTE=void 0;var tk="tag";ie.TAG=tk;var rk="string";ie.STRING=rk;var ik="selector";ie.SELECTOR=ik;var nk="root";ie.ROOT=nk;var sk="pseudo";ie.PSEUDO=sk;var ak="nesting";ie.NESTING=ak;var ok="id";ie.ID=ok;var lk="comment";ie.COMMENT=lk;var uk="combinator";ie.COMBINATOR=uk;var fk="class";ie.CLASS=fk;var ck="attribute";ie.ATTRIBUTE=ck;var pk="universal";ie.UNIVERSAL=pk});var Ln=x((si,Fp)=>{u();"use strict";si.__esModule=!0;si.default=void 0;var dk=mk(dt()),ht=hk(Se());function Mp(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Mp=function(n){return n?t:e})(r)}function hk(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Mp(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function mk(r){return r&&r.__esModule?r:{default:r}}function gk(r,e){var t=typeof Symbol!="undefined"&&r[Symbol.iterator]||r["@@iterator"];if(t)return(t=t.call(r)).next.bind(t);if(Array.isArray(r)||(t=yk(r))||e&&r&&typeof r.length=="number"){t&&(r=t);var i=0;return function(){return i>=r.length?{done:!0}:{done:!1,value:r[i++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function yk(r,e){if(!!r){if(typeof r=="string")return Np(r,e);var t=Object.prototype.toString.call(r).slice(8,-1);if(t==="Object"&&r.constructor&&(t=r.constructor.name),t==="Map"||t==="Set")return Array.from(r);if(t==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t))return Np(r,e)}}function Np(r,e){(e==null||e>r.length)&&(e=r.length);for(var t=0,i=new Array(e);t=n&&(this.indexes[a]=s-1);return this},t.removeAll=function(){for(var n=gk(this.nodes),s;!(s=n()).done;){var a=s.value;a.parent=void 0}return this.nodes=[],this},t.empty=function(){return this.removeAll()},t.insertAfter=function(n,s){s.parent=this;var a=this.index(n);this.nodes.splice(a+1,0,s),s.parent=this;var o;for(var l in this.indexes)o=this.indexes[l],a<=o&&(this.indexes[l]=o+1);return this},t.insertBefore=function(n,s){s.parent=this;var a=this.index(n);this.nodes.splice(a,0,s),s.parent=this;var o;for(var l in this.indexes)o=this.indexes[l],o<=a&&(this.indexes[l]=o+1);return this},t._findChildAtPosition=function(n,s){var a=void 0;return this.each(function(o){if(o.atPosition){var l=o.atPosition(n,s);if(l)return a=l,!1}else if(o.isAtPosition(n,s))return a=o,!1}),a},t.atPosition=function(n,s){if(this.isAtPosition(n,s))return this._findChildAtPosition(n,s)||this},t._inferEndPosition=function(){this.last&&this.last.source&&this.last.source.end&&(this.source=this.source||{},this.source.end=this.source.end||{},Object.assign(this.source.end,this.last.source.end))},t.each=function(n){this.lastEach||(this.lastEach=0),this.indexes||(this.indexes={}),this.lastEach++;var s=this.lastEach;if(this.indexes[s]=0,!!this.length){for(var a,o;this.indexes[s]{u();"use strict";ai.__esModule=!0;ai.default=void 0;var xk=Sk(Ln()),kk=Se();function Sk(r){return r&&r.__esModule?r:{default:r}}function jp(r,e){for(var t=0;t{u();"use strict";oi.__esModule=!0;oi.default=void 0;var Ek=Tk(Ln()),Ok=Se();function Tk(r){return r&&r.__esModule?r:{default:r}}function Rk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,qa(r,e)}function qa(r,e){return qa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},qa(r,e)}var Pk=function(r){Rk(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Ok.SELECTOR,i}return e}(Ek.default);oi.default=Pk;Up.exports=oi.default});var Mn=x((_I,Vp)=>{u();"use strict";var Ik={},Dk=Ik.hasOwnProperty,qk=function(e,t){if(!e)return t;var i={};for(var n in t)i[n]=Dk.call(e,n)?e[n]:t[n];return i},$k=/[ -,\.\/:-@\[-\^`\{-~]/,Lk=/[ -,\.\/:-@\[\]\^`\{-~]/,Mk=/(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g,La=function r(e,t){t=qk(t,r.options),t.quotes!="single"&&t.quotes!="double"&&(t.quotes="single");for(var i=t.quotes=="double"?'"':"'",n=t.isIdentifier,s=e.charAt(0),a="",o=0,l=e.length;o126){if(f>=55296&&f<=56319&&o{u();"use strict";li.__esModule=!0;li.default=void 0;var Nk=Hp(Mn()),Bk=ii(),Fk=Hp(dt()),jk=Se();function Hp(r){return r&&r.__esModule?r:{default:r}}function Wp(r,e){for(var t=0;t{u();"use strict";ui.__esModule=!0;ui.default=void 0;var Hk=Gk(dt()),Wk=Se();function Gk(r){return r&&r.__esModule?r:{default:r}}function Qk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Ba(r,e)}function Ba(r,e){return Ba=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Ba(r,e)}var Yk=function(r){Qk(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=Wk.COMMENT,i}return e}(Hk.default);ui.default=Yk;Qp.exports=ui.default});var za=x((fi,Yp)=>{u();"use strict";fi.__esModule=!0;fi.default=void 0;var Kk=Zk(dt()),Xk=Se();function Zk(r){return r&&r.__esModule?r:{default:r}}function Jk(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ja(r,e)}function ja(r,e){return ja=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ja(r,e)}var eS=function(r){Jk(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=Xk.ID,n}var t=e.prototype;return t.valueToString=function(){return"#"+r.prototype.valueToString.call(this)},e}(Kk.default);fi.default=eS;Yp.exports=fi.default});var Nn=x((ci,Zp)=>{u();"use strict";ci.__esModule=!0;ci.default=void 0;var tS=Kp(Mn()),rS=ii(),iS=Kp(dt());function Kp(r){return r&&r.__esModule?r:{default:r}}function Xp(r,e){for(var t=0;t{u();"use strict";pi.__esModule=!0;pi.default=void 0;var oS=uS(Nn()),lS=Se();function uS(r){return r&&r.__esModule?r:{default:r}}function fS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Va(r,e)}function Va(r,e){return Va=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Va(r,e)}var cS=function(r){fS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=lS.TAG,i}return e}(oS.default);pi.default=cS;Jp.exports=pi.default});var Ga=x((di,ed)=>{u();"use strict";di.__esModule=!0;di.default=void 0;var pS=hS(dt()),dS=Se();function hS(r){return r&&r.__esModule?r:{default:r}}function mS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Wa(r,e)}function Wa(r,e){return Wa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Wa(r,e)}var gS=function(r){mS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=dS.STRING,i}return e}(pS.default);di.default=gS;ed.exports=di.default});var Ya=x((hi,td)=>{u();"use strict";hi.__esModule=!0;hi.default=void 0;var yS=wS(Ln()),bS=Se();function wS(r){return r&&r.__esModule?r:{default:r}}function vS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,Qa(r,e)}function Qa(r,e){return Qa=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},Qa(r,e)}var xS=function(r){vS(e,r);function e(i){var n;return n=r.call(this,i)||this,n.type=bS.PSEUDO,n}var t=e.prototype;return t.toString=function(){var n=this.length?"("+this.map(String).join(",")+")":"";return[this.rawSpaceBefore,this.stringifyProperty("value"),n,this.rawSpaceAfter].join("")},e}(yS.default);hi.default=xS;td.exports=hi.default});var Bn={};Ge(Bn,{deprecate:()=>kS});function kS(r){return r}var Fn=P(()=>{u()});var id=x((EI,rd)=>{u();rd.exports=(Fn(),Bn).deprecate});var to=x(yi=>{u();"use strict";yi.__esModule=!0;yi.default=void 0;yi.unescapeValue=Ja;var mi=Xa(Mn()),SS=Xa(Pn()),AS=Xa(Nn()),CS=Se(),Ka;function Xa(r){return r&&r.__esModule?r:{default:r}}function nd(r,e){for(var t=0;t0&&!n.quoted&&o.before.length===0&&!(n.spaces.value&&n.spaces.value.after)&&(o.before=" "),sd(a,o)}))),s.push("]"),s.push(this.rawSpaceAfter),s.join("")},_S(e,[{key:"quoted",get:function(){var n=this.quoteMark;return n==="'"||n==='"'},set:function(n){RS()}},{key:"quoteMark",get:function(){return this._quoteMark},set:function(n){if(!this._constructed){this._quoteMark=n;return}this._quoteMark!==n&&(this._quoteMark=n,this._syncRawValue())}},{key:"qualifiedAttribute",get:function(){return this.qualifiedName(this.raws.attribute||this.attribute)}},{key:"insensitiveFlag",get:function(){return this.insensitive?"i":""}},{key:"value",get:function(){return this._value},set:function(n){if(this._constructed){var s=Ja(n),a=s.deprecatedUsage,o=s.unescaped,l=s.quoteMark;if(a&&TS(),o===this._value&&l===this._quoteMark)return;this._value=o,this._quoteMark=l,this._syncRawValue()}else this._value=n}},{key:"insensitive",get:function(){return this._insensitive},set:function(n){n||(this._insensitive=!1,this.raws&&(this.raws.insensitiveFlag==="I"||this.raws.insensitiveFlag==="i")&&(this.raws.insensitiveFlag=void 0)),this._insensitive=n}},{key:"attribute",get:function(){return this._attribute},set:function(n){this._handleEscapes("attribute",n),this._attribute=n}}]),e}(AS.default);yi.default=jn;jn.NO_QUOTE=null;jn.SINGLE_QUOTE="'";jn.DOUBLE_QUOTE='"';var eo=(Ka={"'":{quotes:"single",wrap:!0},'"':{quotes:"double",wrap:!0}},Ka[null]={isIdentifier:!0},Ka);function sd(r,e){return""+e.before+r+e.after}});var io=x((bi,ad)=>{u();"use strict";bi.__esModule=!0;bi.default=void 0;var DS=$S(Nn()),qS=Se();function $S(r){return r&&r.__esModule?r:{default:r}}function LS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ro(r,e)}function ro(r,e){return ro=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ro(r,e)}var MS=function(r){LS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=qS.UNIVERSAL,i.value="*",i}return e}(DS.default);bi.default=MS;ad.exports=bi.default});var so=x((wi,od)=>{u();"use strict";wi.__esModule=!0;wi.default=void 0;var NS=FS(dt()),BS=Se();function FS(r){return r&&r.__esModule?r:{default:r}}function jS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,no(r,e)}function no(r,e){return no=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},no(r,e)}var zS=function(r){jS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=BS.COMBINATOR,i}return e}(NS.default);wi.default=zS;od.exports=wi.default});var oo=x((vi,ld)=>{u();"use strict";vi.__esModule=!0;vi.default=void 0;var US=HS(dt()),VS=Se();function HS(r){return r&&r.__esModule?r:{default:r}}function WS(r,e){r.prototype=Object.create(e.prototype),r.prototype.constructor=r,ao(r,e)}function ao(r,e){return ao=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,n){return i.__proto__=n,i},ao(r,e)}var GS=function(r){WS(e,r);function e(t){var i;return i=r.call(this,t)||this,i.type=VS.NESTING,i.value="&",i}return e}(US.default);vi.default=GS;ld.exports=vi.default});var fd=x((zn,ud)=>{u();"use strict";zn.__esModule=!0;zn.default=QS;function QS(r){return r.sort(function(e,t){return e-t})}ud.exports=zn.default});var lo=x(M=>{u();"use strict";M.__esModule=!0;M.word=M.tilde=M.tab=M.str=M.space=M.slash=M.singleQuote=M.semicolon=M.plus=M.pipe=M.openSquare=M.openParenthesis=M.newline=M.greaterThan=M.feed=M.equals=M.doubleQuote=M.dollar=M.cr=M.comment=M.comma=M.combinator=M.colon=M.closeSquare=M.closeParenthesis=M.caret=M.bang=M.backslash=M.at=M.asterisk=M.ampersand=void 0;var YS=38;M.ampersand=YS;var KS=42;M.asterisk=KS;var XS=64;M.at=XS;var ZS=44;M.comma=ZS;var JS=58;M.colon=JS;var eA=59;M.semicolon=eA;var tA=40;M.openParenthesis=tA;var rA=41;M.closeParenthesis=rA;var iA=91;M.openSquare=iA;var nA=93;M.closeSquare=nA;var sA=36;M.dollar=sA;var aA=126;M.tilde=aA;var oA=94;M.caret=oA;var lA=43;M.plus=lA;var uA=61;M.equals=uA;var fA=124;M.pipe=fA;var cA=62;M.greaterThan=cA;var pA=32;M.space=pA;var cd=39;M.singleQuote=cd;var dA=34;M.doubleQuote=dA;var hA=47;M.slash=hA;var mA=33;M.bang=mA;var gA=92;M.backslash=gA;var yA=13;M.cr=yA;var bA=12;M.feed=bA;var wA=10;M.newline=wA;var vA=9;M.tab=vA;var xA=cd;M.str=xA;var kA=-1;M.comment=kA;var SA=-2;M.word=SA;var AA=-3;M.combinator=AA});var hd=x(xi=>{u();"use strict";xi.__esModule=!0;xi.FIELDS=void 0;xi.default=PA;var D=CA(lo()),nr,te;function pd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(pd=function(n){return n?t:e})(r)}function CA(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=pd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}var _A=(nr={},nr[D.tab]=!0,nr[D.newline]=!0,nr[D.cr]=!0,nr[D.feed]=!0,nr),EA=(te={},te[D.space]=!0,te[D.tab]=!0,te[D.newline]=!0,te[D.cr]=!0,te[D.feed]=!0,te[D.ampersand]=!0,te[D.asterisk]=!0,te[D.bang]=!0,te[D.comma]=!0,te[D.colon]=!0,te[D.semicolon]=!0,te[D.openParenthesis]=!0,te[D.closeParenthesis]=!0,te[D.openSquare]=!0,te[D.closeSquare]=!0,te[D.singleQuote]=!0,te[D.doubleQuote]=!0,te[D.plus]=!0,te[D.pipe]=!0,te[D.tilde]=!0,te[D.greaterThan]=!0,te[D.equals]=!0,te[D.dollar]=!0,te[D.caret]=!0,te[D.slash]=!0,te),uo={},dd="0123456789abcdefABCDEF";for(Un=0;Un0?(k=a+v,S=w-y[v].length):(k=a,S=s),T=D.comment,a=k,p=k,d=w-S):c===D.slash?(w=o,T=c,p=a,d=o-s,l=w+1):(w=OA(t,o),T=D.word,p=a,d=w-s),l=w+1;break}e.push([T,a,o-s,p,d,o,l]),S&&(s=S,S=null),o=l}return e}});var kd=x((ki,xd)=>{u();"use strict";ki.__esModule=!0;ki.default=void 0;var IA=je(Da()),fo=je($a()),DA=je(Na()),md=je(Fa()),qA=je(za()),$A=je(Ha()),co=je(Ga()),LA=je(Ya()),gd=Vn(to()),MA=je(io()),po=je(so()),NA=je(oo()),BA=je(fd()),O=Vn(hd()),q=Vn(lo()),FA=Vn(Se()),ue=ii(),Vt,ho;function yd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(yd=function(n){return n?t:e})(r)}function Vn(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=yd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function je(r){return r&&r.__esModule?r:{default:r}}function bd(r,e){for(var t=0;t0){var a=this.current.last;if(a){var o=this.convertWhitespaceNodesToSpace(s),l=o.space,c=o.rawSpace;c!==void 0&&(a.rawSpaceAfter+=c),a.spaces.after+=l}else s.forEach(function(T){return i.newNode(T)})}return}var f=this.currToken,d=void 0;n>this.position&&(d=this.parseWhitespaceEquivalentTokens(n));var p;if(this.isNamedCombinator()?p=this.namedCombinator():this.currToken[O.FIELDS.TYPE]===q.combinator?(p=new po.default({value:this.content(),source:sr(this.currToken),sourceIndex:this.currToken[O.FIELDS.START_POS]}),this.position++):mo[this.currToken[O.FIELDS.TYPE]]||d||this.unexpected(),p){if(d){var h=this.convertWhitespaceNodesToSpace(d),b=h.space,v=h.rawSpace;p.spaces.before=b,p.rawSpaceBefore=v}}else{var y=this.convertWhitespaceNodesToSpace(d,!0),w=y.space,k=y.rawSpace;k||(k=w);var S={},E={spaces:{}};w.endsWith(" ")&&k.endsWith(" ")?(S.before=w.slice(0,w.length-1),E.spaces.before=k.slice(0,k.length-1)):w.startsWith(" ")&&k.startsWith(" ")?(S.after=w.slice(1),E.spaces.after=k.slice(1)):E.value=k,p=new po.default({value:" ",source:go(f,this.tokens[this.position-1]),sourceIndex:f[O.FIELDS.START_POS],spaces:S,raws:E})}return this.currToken&&this.currToken[O.FIELDS.TYPE]===q.space&&(p.spaces.after=this.optionalSpace(this.content()),this.position++),this.newNode(p)},e.comma=function(){if(this.position===this.tokens.length-1){this.root.trailingComma=!0,this.position++;return}this.current._inferEndPosition();var i=new fo.default({source:{start:wd(this.tokens[this.position+1])},sourceIndex:this.tokens[this.position+1][O.FIELDS.START_POS]});this.current.parent.append(i),this.current=i,this.position++},e.comment=function(){var i=this.currToken;this.newNode(new md.default({value:this.content(),source:sr(i),sourceIndex:i[O.FIELDS.START_POS]})),this.position++},e.error=function(i,n){throw this.root.error(i,n)},e.missingBackslash=function(){return this.error("Expected a backslash preceding the semicolon.",{index:this.currToken[O.FIELDS.START_POS]})},e.missingParenthesis=function(){return this.expected("opening parenthesis",this.currToken[O.FIELDS.START_POS])},e.missingSquareBracket=function(){return this.expected("opening square bracket",this.currToken[O.FIELDS.START_POS])},e.unexpected=function(){return this.error("Unexpected '"+this.content()+"'. Escaping special characters with \\ may help.",this.currToken[O.FIELDS.START_POS])},e.unexpectedPipe=function(){return this.error("Unexpected '|'.",this.currToken[O.FIELDS.START_POS])},e.namespace=function(){var i=this.prevToken&&this.content(this.prevToken)||!0;if(this.nextToken[O.FIELDS.TYPE]===q.word)return this.position++,this.word(i);if(this.nextToken[O.FIELDS.TYPE]===q.asterisk)return this.position++,this.universal(i);this.unexpectedPipe()},e.nesting=function(){if(this.nextToken){var i=this.content(this.nextToken);if(i==="|"){this.position++;return}}var n=this.currToken;this.newNode(new NA.default({value:this.content(),source:sr(n),sourceIndex:n[O.FIELDS.START_POS]})),this.position++},e.parentheses=function(){var i=this.current.last,n=1;if(this.position++,i&&i.type===FA.PSEUDO){var s=new fo.default({source:{start:wd(this.tokens[this.position])},sourceIndex:this.tokens[this.position][O.FIELDS.START_POS]}),a=this.current;for(i.append(s),this.current=s;this.position1&&i.nextToken&&i.nextToken[O.FIELDS.TYPE]===q.openParenthesis&&i.error("Misplaced parenthesis.",{index:i.nextToken[O.FIELDS.START_POS]})});else return this.expected(["pseudo-class","pseudo-element"],this.currToken[O.FIELDS.START_POS])},e.space=function(){var i=this.content();this.position===0||this.prevToken[O.FIELDS.TYPE]===q.comma||this.prevToken[O.FIELDS.TYPE]===q.openParenthesis||this.current.nodes.every(function(n){return n.type==="comment"})?(this.spaces=this.optionalSpace(i),this.position++):this.position===this.tokens.length-1||this.nextToken[O.FIELDS.TYPE]===q.comma||this.nextToken[O.FIELDS.TYPE]===q.closeParenthesis?(this.current.last.spaces.after=this.optionalSpace(i),this.position++):this.combinator()},e.string=function(){var i=this.currToken;this.newNode(new co.default({value:this.content(),source:sr(i),sourceIndex:i[O.FIELDS.START_POS]})),this.position++},e.universal=function(i){var n=this.nextToken;if(n&&this.content(n)==="|")return this.position++,this.namespace();var s=this.currToken;this.newNode(new MA.default({value:this.content(),source:sr(s),sourceIndex:s[O.FIELDS.START_POS]}),i),this.position++},e.splitWord=function(i,n){for(var s=this,a=this.nextToken,o=this.content();a&&~[q.dollar,q.caret,q.equals,q.word].indexOf(a[O.FIELDS.TYPE]);){this.position++;var l=this.content();if(o+=l,l.lastIndexOf("\\")===l.length-1){var c=this.nextToken;c&&c[O.FIELDS.TYPE]===q.space&&(o+=this.requiredSpace(this.content(c)),this.position++)}a=this.nextToken}var f=yo(o,".").filter(function(b){var v=o[b-1]==="\\",y=/^\d+\.\d+%$/.test(o);return!v&&!y}),d=yo(o,"#").filter(function(b){return o[b-1]!=="\\"}),p=yo(o,"#{");p.length&&(d=d.filter(function(b){return!~p.indexOf(b)}));var h=(0,BA.default)(UA([0].concat(f,d)));h.forEach(function(b,v){var y=h[v+1]||o.length,w=o.slice(b,y);if(v===0&&n)return n.call(s,w,h.length);var k,S=s.currToken,E=S[O.FIELDS.START_POS]+h[v],T=Ht(S[1],S[2]+b,S[3],S[2]+(y-1));if(~f.indexOf(b)){var B={value:w.slice(1),source:T,sourceIndex:E};k=new DA.default(ar(B,"value"))}else if(~d.indexOf(b)){var N={value:w.slice(1),source:T,sourceIndex:E};k=new qA.default(ar(N,"value"))}else{var R={value:w,source:T,sourceIndex:E};ar(R,"value"),k=new $A.default(R)}s.newNode(k,i),i=null}),this.position++},e.word=function(i){var n=this.nextToken;return n&&this.content(n)==="|"?(this.position++,this.namespace()):this.splitWord(i)},e.loop=function(){for(;this.position{u();"use strict";Si.__esModule=!0;Si.default=void 0;var HA=WA(kd());function WA(r){return r&&r.__esModule?r:{default:r}}var GA=function(){function r(t,i){this.func=t||function(){},this.funcRes=null,this.options=i}var e=r.prototype;return e._shouldUpdateSelector=function(i,n){n===void 0&&(n={});var s=Object.assign({},this.options,n);return s.updateSelector===!1?!1:typeof i!="string"},e._isLossy=function(i){i===void 0&&(i={});var n=Object.assign({},this.options,i);return n.lossless===!1},e._root=function(i,n){n===void 0&&(n={});var s=new HA.default(i,this._parseOptions(n));return s.root},e._parseOptions=function(i){return{lossy:this._isLossy(i)}},e._run=function(i,n){var s=this;return n===void 0&&(n={}),new Promise(function(a,o){try{var l=s._root(i,n);Promise.resolve(s.func(l)).then(function(c){var f=void 0;return s._shouldUpdateSelector(i,n)&&(f=l.toString(),i.selector=f),{transform:c,root:l,string:f}}).then(a,o)}catch(c){o(c);return}})},e._runSync=function(i,n){n===void 0&&(n={});var s=this._root(i,n),a=this.func(s);if(a&&typeof a.then=="function")throw new Error("Selector processor returned a promise to a synchronous call.");var o=void 0;return n.updateSelector&&typeof i!="string"&&(o=s.toString(),i.selector=o),{transform:a,root:s,string:o}},e.ast=function(i,n){return this._run(i,n).then(function(s){return s.root})},e.astSync=function(i,n){return this._runSync(i,n).root},e.transform=function(i,n){return this._run(i,n).then(function(s){return s.transform})},e.transformSync=function(i,n){return this._runSync(i,n).transform},e.process=function(i,n){return this._run(i,n).then(function(s){return s.string||s.root.toString()})},e.processSync=function(i,n){var s=this._runSync(i,n);return s.string||s.root.toString()},r}();Si.default=GA;Sd.exports=Si.default});var Cd=x(ne=>{u();"use strict";ne.__esModule=!0;ne.universal=ne.tag=ne.string=ne.selector=ne.root=ne.pseudo=ne.nesting=ne.id=ne.comment=ne.combinator=ne.className=ne.attribute=void 0;var QA=ze(to()),YA=ze(Na()),KA=ze(so()),XA=ze(Fa()),ZA=ze(za()),JA=ze(oo()),eC=ze(Ya()),tC=ze(Da()),rC=ze($a()),iC=ze(Ga()),nC=ze(Ha()),sC=ze(io());function ze(r){return r&&r.__esModule?r:{default:r}}var aC=function(e){return new QA.default(e)};ne.attribute=aC;var oC=function(e){return new YA.default(e)};ne.className=oC;var lC=function(e){return new KA.default(e)};ne.combinator=lC;var uC=function(e){return new XA.default(e)};ne.comment=uC;var fC=function(e){return new ZA.default(e)};ne.id=fC;var cC=function(e){return new JA.default(e)};ne.nesting=cC;var pC=function(e){return new eC.default(e)};ne.pseudo=pC;var dC=function(e){return new tC.default(e)};ne.root=dC;var hC=function(e){return new rC.default(e)};ne.selector=hC;var mC=function(e){return new iC.default(e)};ne.string=mC;var gC=function(e){return new nC.default(e)};ne.tag=gC;var yC=function(e){return new sC.default(e)};ne.universal=yC});var Td=x(Z=>{u();"use strict";Z.__esModule=!0;Z.isComment=Z.isCombinator=Z.isClassName=Z.isAttribute=void 0;Z.isContainer=TC;Z.isIdentifier=void 0;Z.isNamespace=RC;Z.isNesting=void 0;Z.isNode=bo;Z.isPseudo=void 0;Z.isPseudoClass=OC;Z.isPseudoElement=Od;Z.isUniversal=Z.isTag=Z.isString=Z.isSelector=Z.isRoot=void 0;var fe=Se(),Oe,bC=(Oe={},Oe[fe.ATTRIBUTE]=!0,Oe[fe.CLASS]=!0,Oe[fe.COMBINATOR]=!0,Oe[fe.COMMENT]=!0,Oe[fe.ID]=!0,Oe[fe.NESTING]=!0,Oe[fe.PSEUDO]=!0,Oe[fe.ROOT]=!0,Oe[fe.SELECTOR]=!0,Oe[fe.STRING]=!0,Oe[fe.TAG]=!0,Oe[fe.UNIVERSAL]=!0,Oe);function bo(r){return typeof r=="object"&&bC[r.type]}function Ue(r,e){return bo(e)&&e.type===r}var _d=Ue.bind(null,fe.ATTRIBUTE);Z.isAttribute=_d;var wC=Ue.bind(null,fe.CLASS);Z.isClassName=wC;var vC=Ue.bind(null,fe.COMBINATOR);Z.isCombinator=vC;var xC=Ue.bind(null,fe.COMMENT);Z.isComment=xC;var kC=Ue.bind(null,fe.ID);Z.isIdentifier=kC;var SC=Ue.bind(null,fe.NESTING);Z.isNesting=SC;var wo=Ue.bind(null,fe.PSEUDO);Z.isPseudo=wo;var AC=Ue.bind(null,fe.ROOT);Z.isRoot=AC;var CC=Ue.bind(null,fe.SELECTOR);Z.isSelector=CC;var _C=Ue.bind(null,fe.STRING);Z.isString=_C;var Ed=Ue.bind(null,fe.TAG);Z.isTag=Ed;var EC=Ue.bind(null,fe.UNIVERSAL);Z.isUniversal=EC;function Od(r){return wo(r)&&r.value&&(r.value.startsWith("::")||r.value.toLowerCase()===":before"||r.value.toLowerCase()===":after"||r.value.toLowerCase()===":first-letter"||r.value.toLowerCase()===":first-line")}function OC(r){return wo(r)&&!Od(r)}function TC(r){return!!(bo(r)&&r.walk)}function RC(r){return _d(r)||Ed(r)}});var Rd=x(Ke=>{u();"use strict";Ke.__esModule=!0;var vo=Se();Object.keys(vo).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===vo[r]||(Ke[r]=vo[r])});var xo=Cd();Object.keys(xo).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===xo[r]||(Ke[r]=xo[r])});var ko=Td();Object.keys(ko).forEach(function(r){r==="default"||r==="__esModule"||r in Ke&&Ke[r]===ko[r]||(Ke[r]=ko[r])})});var it=x((Ai,Id)=>{u();"use strict";Ai.__esModule=!0;Ai.default=void 0;var PC=qC(Ad()),IC=DC(Rd());function Pd(r){if(typeof WeakMap!="function")return null;var e=new WeakMap,t=new WeakMap;return(Pd=function(n){return n?t:e})(r)}function DC(r,e){if(!e&&r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var t=Pd(e);if(t&&t.has(r))return t.get(r);var i={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in r)if(s!=="default"&&Object.prototype.hasOwnProperty.call(r,s)){var a=n?Object.getOwnPropertyDescriptor(r,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=r[s]}return i.default=r,t&&t.set(r,i),i}function qC(r){return r&&r.__esModule?r:{default:r}}var So=function(e){return new PC.default(e)};Object.assign(So,IC);delete So.__esModule;var $C=So;Ai.default=$C;Id.exports=Ai.default});function mt(r){return["fontSize","outline"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e[0]),e):r==="fontFamily"?e=>{typeof e=="function"&&(e=e({}));let t=Array.isArray(e)&&ke(e[1])?e[0]:e;return Array.isArray(t)?t.join(", "):t}:["boxShadow","transitionProperty","transitionDuration","transitionDelay","transitionTimingFunction","backgroundImage","backgroundSize","backgroundColor","cursor","animation"].includes(r)?e=>(typeof e=="function"&&(e=e({})),Array.isArray(e)&&(e=e.join(", ")),e):["gridTemplateColumns","gridTemplateRows","objectPosition"].includes(r)?e=>(typeof e=="function"&&(e=e({})),typeof e=="string"&&(e=ee.list.comma(e).join(" ")),e):(e,t={})=>(typeof e=="function"&&(e=e(t)),e)}var Ci=P(()=>{u();Ot();Kt()});var Bd=x((MI,Oo)=>{u();var{AtRule:LC,Rule:Dd}=$e(),qd=it();function Ao(r,e){let t;try{qd(i=>{t=i}).processSync(r)}catch(i){throw r.includes(":")?e?e.error("Missed semicolon"):i:e?e.error(i.message):i}return t.at(0)}function $d(r,e){let t=!1;return r.each(i=>{if(i.type==="nesting"){let n=e.clone({});i.value!=="&"?i.replaceWith(Ao(i.value.replace("&",n.toString()))):i.replaceWith(n),t=!0}else"nodes"in i&&i.nodes&&$d(i,e)&&(t=!0)}),t}function Ld(r,e){let t=[];return r.selectors.forEach(i=>{let n=Ao(i,r);e.selectors.forEach(s=>{if(!s)return;let a=Ao(s,e);$d(a,n)||(a.prepend(qd.combinator({value:" "})),a.prepend(n.clone({}))),t.push(a.toString())})}),t}function Hn(r,e){let t=r.prev();for(e.after(r);t&&t.type==="comment";){let i=t.prev();e.after(t),t=i}return r}function MC(r){return function e(t,i,n,s=n){let a=[];if(i.each(o=>{o.type==="rule"&&n?s&&(o.selectors=Ld(t,o)):o.type==="atrule"&&o.nodes?r[o.name]?e(t,o,s):i[_o]!==!1&&a.push(o):a.push(o)}),n&&a.length){let o=t.clone({nodes:[]});for(let l of a)o.append(l);i.prepend(o)}}}function Co(r,e,t){let i=new Dd({nodes:[],selector:r});return i.append(e),t.after(i),i}function Md(r,e){let t={};for(let i of r)t[i]=!0;if(e)for(let i of e)t[i.replace(/^@/,"")]=!0;return t}function NC(r){r=r.trim();let e=r.match(/^\((.*)\)$/);if(!e)return{selector:r,type:"basic"};let t=e[1].match(/^(with(?:out)?):(.+)$/);if(t){let i=t[1]==="with",n=Object.fromEntries(t[2].trim().split(/\s+/).map(a=>[a,!0]));if(i&&n.all)return{type:"noop"};let s=a=>!!n[a];return n.all?s=()=>!0:i&&(s=a=>a==="all"?!1:!n[a]),{escapes:s,type:"withrules"}}return{type:"unknown"}}function BC(r){let e=[],t=r.parent;for(;t&&t instanceof LC;)e.push(t),t=t.parent;return e}function FC(r){let e=r[Nd];if(!e)r.after(r.nodes);else{let t=r.nodes,i,n=-1,s,a,o,l=BC(r);if(l.forEach((c,f)=>{if(e(c.name))i=c,n=f,a=o;else{let d=o;o=c.clone({nodes:[]}),d&&o.append(d),s=s||o}}),i?a?(s.append(t),i.after(a)):i.after(t):r.after(t),r.next()&&i){let c;l.slice(0,n+1).forEach((f,d,p)=>{let h=c;c=f.clone({nodes:[]}),h&&c.append(h);let b=[],y=(p[d-1]||r).next();for(;y;)b.push(y),y=y.next();c.append(b)}),c&&(a||t[t.length-1]).after(c)}}r.remove()}var _o=Symbol("rootRuleMergeSel"),Nd=Symbol("rootRuleEscapes");function jC(r){let{params:e}=r,{escapes:t,selector:i,type:n}=NC(e);if(n==="unknown")throw r.error(`Unknown @${r.name} parameter ${JSON.stringify(e)}`);if(n==="basic"&&i){let s=new Dd({nodes:r.nodes,selector:i});r.removeAll(),r.append(s)}r[Nd]=t,r[_o]=t?!t("all"):n==="noop"}var Eo=Symbol("hasRootRule");Oo.exports=(r={})=>{let e=Md(["media","supports","layer","container","starting-style"],r.bubble),t=MC(e),i=Md(["document","font-face","keyframes","-webkit-keyframes","-moz-keyframes"],r.unwrap),n=(r.rootRuleName||"at-root").replace(/^@/,""),s=r.preserveEmpty;return{Once(a){a.walkAtRules(n,o=>{jC(o),a[Eo]=!0})},postcssPlugin:"postcss-nested",RootExit(a){a[Eo]&&(a.walkAtRules(n,FC),a[Eo]=!1)},Rule(a){let o=!1,l=a,c=!1,f=[];a.each(d=>{d.type==="rule"?(f.length&&(l=Co(a.selector,f,l),f=[]),c=!0,o=!0,d.selectors=Ld(a,d),l=Hn(d,l)):d.type==="atrule"?(f.length&&(l=Co(a.selector,f,l),f=[]),d.name===n?(o=!0,t(a,d,!0,d[_o]),l=Hn(d,l)):e[d.name]?(c=!0,o=!0,t(a,d,!0),l=Hn(d,l)):i[d.name]?(c=!0,o=!0,t(a,d,!1),l=Hn(d,l)):c&&f.push(d)):d.type==="decl"&&c&&f.push(d)}),f.length&&(l=Co(a.selector,f,l)),o&&s!==!0&&(a.raws.semicolon=!0,a.nodes.length===0&&a.remove())}}};Oo.exports.postcss=!0});var Ud=x((NI,zd)=>{u();"use strict";var Fd=/-(\w|$)/g,jd=(r,e)=>e.toUpperCase(),zC=r=>(r=r.toLowerCase(),r==="float"?"cssFloat":r.startsWith("-ms-")?r.substr(1).replace(Fd,jd):r.replace(Fd,jd));zd.exports=zC});var Po=x((BI,Vd)=>{u();var UC=Ud(),VC={boxFlex:!0,boxFlexGroup:!0,columnCount:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,strokeDashoffset:!0,strokeOpacity:!0,strokeWidth:!0};function To(r){return typeof r.nodes=="undefined"?!0:Ro(r)}function Ro(r){let e,t={};return r.each(i=>{if(i.type==="atrule")e="@"+i.name,i.params&&(e+=" "+i.params),typeof t[e]=="undefined"?t[e]=To(i):Array.isArray(t[e])?t[e].push(To(i)):t[e]=[t[e],To(i)];else if(i.type==="rule"){let n=Ro(i);if(t[i.selector])for(let s in n)t[i.selector][s]=n[s];else t[i.selector]=n}else if(i.type==="decl"){i.prop[0]==="-"&&i.prop[1]==="-"||i.parent&&i.parent.selector===":export"?e=i.prop:e=UC(i.prop);let n=i.value;!isNaN(i.value)&&VC[e]&&(n=parseFloat(i.value)),i.important&&(n+=" !important"),typeof t[e]=="undefined"?t[e]=n:Array.isArray(t[e])?t[e].push(n):t[e]=[t[e],n]}}),t}Vd.exports=Ro});var Wn=x((FI,Qd)=>{u();var _i=$e(),Hd=/\s*!important\s*$/i,HC={"box-flex":!0,"box-flex-group":!0,"column-count":!0,flex:!0,"flex-grow":!0,"flex-positive":!0,"flex-shrink":!0,"flex-negative":!0,"font-weight":!0,"line-clamp":!0,"line-height":!0,opacity:!0,order:!0,orphans:!0,"tab-size":!0,widows:!0,"z-index":!0,zoom:!0,"fill-opacity":!0,"stroke-dashoffset":!0,"stroke-opacity":!0,"stroke-width":!0};function WC(r){return r.replace(/([A-Z])/g,"-$1").replace(/^ms-/,"-ms-").toLowerCase()}function Wd(r,e,t){t===!1||t===null||(e.startsWith("--")||(e=WC(e)),typeof t=="number"&&(t===0||HC[e]?t=t.toString():t+="px"),e==="css-float"&&(e="float"),Hd.test(t)?(t=t.replace(Hd,""),r.push(_i.decl({prop:e,value:t,important:!0}))):r.push(_i.decl({prop:e,value:t})))}function Gd(r,e,t){let i=_i.atRule({name:e[1],params:e[3]||""});typeof t=="object"&&(i.nodes=[],Io(t,i)),r.push(i)}function Io(r,e){let t,i,n;for(t in r)if(i=r[t],!(i===null||typeof i=="undefined"))if(t[0]==="@"){let s=t.match(/@(\S+)(\s+([\W\w]*)\s*)?/);if(Array.isArray(i))for(let a of i)Gd(e,s,a);else Gd(e,s,i)}else if(Array.isArray(i))for(let s of i)Wd(e,t,s);else typeof i=="object"?(n=_i.rule({selector:t}),Io(i,n),e.push(n)):Wd(e,t,i)}Qd.exports=function(r){let e=_i.root();return Io(r,e),e}});var Do=x((jI,Yd)=>{u();var GC=Po();Yd.exports=function(e){return console&&console.warn&&e.warnings().forEach(t=>{let i=t.plugin||"PostCSS";console.warn(i+": "+t.text)}),GC(e.root)}});var Xd=x((zI,Kd)=>{u();var QC=$e(),YC=Do(),KC=Wn();Kd.exports=function(e){let t=QC(e);return async i=>{let n=await t.process(i,{parser:KC,from:void 0});return YC(n)}}});var Jd=x((UI,Zd)=>{u();var XC=$e(),ZC=Do(),JC=Wn();Zd.exports=function(r){let e=XC(r);return t=>{let i=e.process(t,{parser:JC,from:void 0});return ZC(i)}}});var th=x((VI,eh)=>{u();var e_=Po(),t_=Wn(),r_=Xd(),i_=Jd();eh.exports={objectify:e_,parse:t_,async:r_,sync:i_}});var or,rh,HI,WI,GI,QI,ih=P(()=>{u();or=pe(th()),rh=or.default,HI=or.default.objectify,WI=or.default.parse,GI=or.default.async,QI=or.default.sync});function lr(r){return Array.isArray(r)?r.flatMap(e=>ee([(0,nh.default)({bubble:["screen"]})]).process(e,{parser:rh}).root.nodes):lr([r])}var nh,qo=P(()=>{u();Ot();nh=pe(Bd());ih()});function ur(r,e,t=!1){if(r==="")return e;let i=typeof e=="string"?(0,sh.default)().astSync(e):e;return i.walkClasses(n=>{let s=n.value,a=t&&s.startsWith("-");n.value=a?`-${r}${s.slice(1)}`:`${r}${s}`}),typeof e=="string"?i.toString():i}var sh,Gn=P(()=>{u();sh=pe(it())});function Te(r){let e=ah.default.className();return e.value=r,jt(e?.raws?.value??e.value)}var ah,fr=P(()=>{u();ah=pe(it());Zi()});function $o(r){return jt(`.${Te(r)}`)}function Qn(r,e){return $o(Ei(r,e))}function Ei(r,e){return e==="DEFAULT"?r:e==="-"||e==="-DEFAULT"?`-${r}`:e.startsWith("-")?`-${r}${e}`:e.startsWith("/")?`${r}${e}`:`${r}-${e}`}var Lo=P(()=>{u();fr();Zi()});function L(r,e=[[r,[r]]],{filterDefault:t=!1,...i}={}){let n=mt(r);return function({matchUtilities:s,theme:a}){for(let o of e){let l=Array.isArray(o[0])?o:[o];s(l.reduce((c,[f,d])=>Object.assign(c,{[f]:p=>d.reduce((h,b)=>Array.isArray(b)?Object.assign(h,{[b[0]]:b[1]}):Object.assign(h,{[b]:n(p)}),{})}),{}),{...i,values:t?Object.fromEntries(Object.entries(a(r)??{}).filter(([c])=>c!=="DEFAULT")):a(r)})}}}var oh=P(()=>{u();Ci()});function Tt(r){return r=Array.isArray(r)?r:[r],r.map(e=>{let t=e.values.map(i=>i.raw!==void 0?i.raw:[i.min&&`(min-width: ${i.min})`,i.max&&`(max-width: ${i.max})`].filter(Boolean).join(" and "));return e.not?`not all and ${t}`:t}).join(", ")}var Yn=P(()=>{u()});function Mo(r){return r.split(f_).map(t=>{let i=t.trim(),n={value:i},s=i.split(c_),a=new Set;for(let o of s)!a.has("DIRECTIONS")&&n_.has(o)?(n.direction=o,a.add("DIRECTIONS")):!a.has("PLAY_STATES")&&s_.has(o)?(n.playState=o,a.add("PLAY_STATES")):!a.has("FILL_MODES")&&a_.has(o)?(n.fillMode=o,a.add("FILL_MODES")):!a.has("ITERATION_COUNTS")&&(o_.has(o)||p_.test(o))?(n.iterationCount=o,a.add("ITERATION_COUNTS")):!a.has("TIMING_FUNCTION")&&l_.has(o)||!a.has("TIMING_FUNCTION")&&u_.some(l=>o.startsWith(`${l}(`))?(n.timingFunction=o,a.add("TIMING_FUNCTION")):!a.has("DURATION")&&lh.test(o)?(n.duration=o,a.add("DURATION")):!a.has("DELAY")&&lh.test(o)?(n.delay=o,a.add("DELAY")):a.has("NAME")?(n.unknown||(n.unknown=[]),n.unknown.push(o)):(n.name=o,a.add("NAME"));return n})}var n_,s_,a_,o_,l_,u_,f_,c_,lh,p_,uh=P(()=>{u();n_=new Set(["normal","reverse","alternate","alternate-reverse"]),s_=new Set(["running","paused"]),a_=new Set(["none","forwards","backwards","both"]),o_=new Set(["infinite"]),l_=new Set(["linear","ease","ease-in","ease-out","ease-in-out","step-start","step-end"]),u_=["cubic-bezier","steps"],f_=/\,(?![^(]*\))/g,c_=/\ +(?![^(]*\))/g,lh=/^(-?[\d.]+m?s)$/,p_=/^(\d+)$/});var fh,xe,ch=P(()=>{u();fh=r=>Object.assign({},...Object.entries(r??{}).flatMap(([e,t])=>typeof t=="object"?Object.entries(fh(t)).map(([i,n])=>({[e+(i==="DEFAULT"?"":`-${i}`)]:n})):[{[`${e}`]:t}])),xe=fh});var dh,ph=P(()=>{dh="3.4.17"});function Rt(r,e=!0){return Array.isArray(r)?r.map(t=>{if(e&&Array.isArray(t))throw new Error("The tuple syntax is not supported for `screens`.");if(typeof t=="string")return{name:t.toString(),not:!1,values:[{min:t,max:void 0}]};let[i,n]=t;return i=i.toString(),typeof n=="string"?{name:i,not:!1,values:[{min:n,max:void 0}]}:Array.isArray(n)?{name:i,not:!1,values:n.map(s=>mh(s))}:{name:i,not:!1,values:[mh(n)]}}):Rt(Object.entries(r??{}),!1)}function Kn(r){return r.values.length!==1?{result:!1,reason:"multiple-values"}:r.values[0].raw!==void 0?{result:!1,reason:"raw-values"}:r.values[0].min!==void 0&&r.values[0].max!==void 0?{result:!1,reason:"min-and-max"}:{result:!0,reason:null}}function hh(r,e,t){let i=Xn(e,r),n=Xn(t,r),s=Kn(i),a=Kn(n);if(s.reason==="multiple-values"||a.reason==="multiple-values")throw new Error("Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.");if(s.reason==="raw-values"||a.reason==="raw-values")throw new Error("Attempted to sort a screen with raw values. This should never happen. Please open a bug report.");if(s.reason==="min-and-max"||a.reason==="min-and-max")throw new Error("Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.");let{min:o,max:l}=i.values[0],{min:c,max:f}=n.values[0];e.not&&([o,l]=[l,o]),t.not&&([c,f]=[f,c]),o=o===void 0?o:parseFloat(o),l=l===void 0?l:parseFloat(l),c=c===void 0?c:parseFloat(c),f=f===void 0?f:parseFloat(f);let[d,p]=r==="min"?[o,c]:[f,l];return d-p}function Xn(r,e){return typeof r=="object"?r:{name:"arbitrary-screen",values:[{[e]:r}]}}function mh({"min-width":r,min:e=r,max:t,raw:i}={}){return{min:e,max:t,raw:i}}var Zn=P(()=>{u()});function Jn(r,e){r.walkDecls(t=>{if(e.includes(t.prop)){t.remove();return}for(let i of e)t.value.includes(`/ var(${i})`)?t.value=t.value.replace(`/ var(${i})`,""):t.value.includes(`/ var(${i}, 1)`)&&(t.value=t.value.replace(`/ var(${i}, 1)`,""))})}var gh=P(()=>{u()});var se,Xe,nt,ge,yh,bh=P(()=>{u();ft();et();Ot();oh();Yn();fr();uh();ch();Lr();ra();Kt();Ci();ph();Be();Zn();Ys();gh();ct();Br();Oi();se={childVariant:({addVariant:r})=>{r("*","& > *")},pseudoElementVariants:({addVariant:r})=>{r("first-letter","&::first-letter"),r("first-line","&::first-line"),r("marker",[({container:e})=>(Jn(e,["--tw-text-opacity"]),"& *::marker"),({container:e})=>(Jn(e,["--tw-text-opacity"]),"&::marker")]),r("selection",["& *::selection","&::selection"]),r("file","&::file-selector-button"),r("placeholder","&::placeholder"),r("backdrop","&::backdrop"),r("before",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(ee.decl({prop:"content",value:"var(--tw-content)"}))}),"&::before")),r("after",({container:e})=>(e.walkRules(t=>{let i=!1;t.walkDecls("content",()=>{i=!0}),i||t.prepend(ee.decl({prop:"content",value:"var(--tw-content)"}))}),"&::after"))},pseudoClassVariants:({addVariant:r,matchVariant:e,config:t,prefix:i})=>{let n=[["first","&:first-child"],["last","&:last-child"],["only","&:only-child"],["odd","&:nth-child(odd)"],["even","&:nth-child(even)"],"first-of-type","last-of-type","only-of-type",["visited",({container:a})=>(Jn(a,["--tw-text-opacity","--tw-border-opacity","--tw-bg-opacity"]),"&:visited")],"target",["open","&[open]"],"default","checked","indeterminate","placeholder-shown","autofill","optional","required","valid","invalid","in-range","out-of-range","read-only","empty","focus-within",["hover",we(t(),"hoverOnlyWhenSupported")?"@media (hover: hover) and (pointer: fine) { &:hover }":"&:hover"],"focus","focus-visible","active","enabled","disabled"].map(a=>Array.isArray(a)?a:[a,`&:${a}`]);for(let[a,o]of n)r(a,l=>typeof o=="function"?o(l):o);let s={group:(a,{modifier:o})=>o?[`:merge(${i(".group")}\\/${Te(o)})`," &"]:[`:merge(${i(".group")})`," &"],peer:(a,{modifier:o})=>o?[`:merge(${i(".peer")}\\/${Te(o)})`," ~ &"]:[`:merge(${i(".peer")})`," ~ &"]};for(let[a,o]of Object.entries(s))e(a,(l="",c)=>{let f=K(typeof l=="function"?l(c):l);f.includes("&")||(f="&"+f);let[d,p]=o("",c),h=null,b=null,v=0;for(let y=0;y{r("ltr",'&:where([dir="ltr"], [dir="ltr"] *)'),r("rtl",'&:where([dir="rtl"], [dir="rtl"] *)')},reducedMotionVariants:({addVariant:r})=>{r("motion-safe","@media (prefers-reduced-motion: no-preference)"),r("motion-reduce","@media (prefers-reduced-motion: reduce)")},darkVariants:({config:r,addVariant:e})=>{let[t,i=".dark"]=[].concat(r("darkMode","media"));if(t===!1&&(t="media",G.warn("darkmode-false",["The `darkMode` option in your Tailwind CSS configuration is set to `false`, which now behaves the same as `media`.","Change `darkMode` to `media` or remove it entirely.","https://tailwindcss.com/docs/upgrade-guide#remove-dark-mode-configuration"])),t==="variant"){let n;if(Array.isArray(i)||typeof i=="function"?n=i:typeof i=="string"&&(n=[i]),Array.isArray(n))for(let s of n)s===".dark"?(t=!1,G.warn("darkmode-variant-without-selector",["When using `variant` for `darkMode`, you must provide a selector.",'Example: `darkMode: ["variant", ".your-selector &"]`'])):s.includes("&")||(t=!1,G.warn("darkmode-variant-without-ampersand",["When using `variant` for `darkMode`, your selector must contain `&`.",'Example `darkMode: ["variant", ".your-selector &"]`']));i=n}t==="selector"?e("dark",`&:where(${i}, ${i} *)`):t==="media"?e("dark","@media (prefers-color-scheme: dark)"):t==="variant"?e("dark",i):t==="class"&&e("dark",`&:is(${i} *)`)},printVariant:({addVariant:r})=>{r("print","@media print")},screenVariants:({theme:r,addVariant:e,matchVariant:t})=>{let i=r("screens")??{},n=Object.values(i).every(w=>typeof w=="string"),s=Rt(r("screens")),a=new Set([]);function o(w){return w.match(/(\D+)$/)?.[1]??"(none)"}function l(w){w!==void 0&&a.add(o(w))}function c(w){return l(w),a.size===1}for(let w of s)for(let k of w.values)l(k.min),l(k.max);let f=a.size<=1;function d(w){return Object.fromEntries(s.filter(k=>Kn(k).result).map(k=>{let{min:S,max:E}=k.values[0];if(w==="min"&&S!==void 0)return k;if(w==="min"&&E!==void 0)return{...k,not:!k.not};if(w==="max"&&E!==void 0)return k;if(w==="max"&&S!==void 0)return{...k,not:!k.not}}).map(k=>[k.name,k]))}function p(w){return(k,S)=>hh(w,k.value,S.value)}let h=p("max"),b=p("min");function v(w){return k=>{if(n)if(f){if(typeof k=="string"&&!c(k))return G.warn("minmax-have-mixed-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[]}else return G.warn("mixed-screen-units",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing mixed units."]),[];else return G.warn("complex-screen-config",["The `min-*` and `max-*` variants are not supported with a `screens` configuration containing objects."]),[];return[`@media ${Tt(Xn(k,w))}`]}}t("max",v("max"),{sort:h,values:n?d("max"):{}});let y="min-screens";for(let w of s)e(w.name,`@media ${Tt(w)}`,{id:y,sort:n&&f?b:void 0,value:w});t("min",v("min"),{id:y,sort:b})},supportsVariants:({matchVariant:r,theme:e})=>{r("supports",(t="")=>{let i=K(t),n=/^\w*\s*\(/.test(i);return i=n?i.replace(/\b(and|or|not)\b/g," $1 "):i,n?`@supports ${i}`:(i.includes(":")||(i=`${i}: var(--tw)`),i.startsWith("(")&&i.endsWith(")")||(i=`(${i})`),`@supports ${i}`)},{values:e("supports")??{}})},hasVariants:({matchVariant:r,prefix:e})=>{r("has",t=>`&:has(${K(t)})`,{values:{},[Pt]:{respectPrefix:!1}}),r("group-has",(t,{modifier:i})=>i?`:merge(${e(".group")}\\/${i}):has(${K(t)}) &`:`:merge(${e(".group")}):has(${K(t)}) &`,{values:{},[Pt]:{respectPrefix:!1}}),r("peer-has",(t,{modifier:i})=>i?`:merge(${e(".peer")}\\/${i}):has(${K(t)}) ~ &`:`:merge(${e(".peer")}):has(${K(t)}) ~ &`,{values:{},[Pt]:{respectPrefix:!1}})},ariaVariants:({matchVariant:r,theme:e})=>{r("aria",t=>`&[aria-${Ye(K(t))}]`,{values:e("aria")??{}}),r("group-aria",(t,{modifier:i})=>i?`:merge(.group\\/${i})[aria-${Ye(K(t))}] &`:`:merge(.group)[aria-${Ye(K(t))}] &`,{values:e("aria")??{}}),r("peer-aria",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[aria-${Ye(K(t))}] ~ &`:`:merge(.peer)[aria-${Ye(K(t))}] ~ &`,{values:e("aria")??{}})},dataVariants:({matchVariant:r,theme:e})=>{r("data",t=>`&[data-${Ye(K(t))}]`,{values:e("data")??{}}),r("group-data",(t,{modifier:i})=>i?`:merge(.group\\/${i})[data-${Ye(K(t))}] &`:`:merge(.group)[data-${Ye(K(t))}] &`,{values:e("data")??{}}),r("peer-data",(t,{modifier:i})=>i?`:merge(.peer\\/${i})[data-${Ye(K(t))}] ~ &`:`:merge(.peer)[data-${Ye(K(t))}] ~ &`,{values:e("data")??{}})},orientationVariants:({addVariant:r})=>{r("portrait","@media (orientation: portrait)"),r("landscape","@media (orientation: landscape)")},prefersContrastVariants:({addVariant:r})=>{r("contrast-more","@media (prefers-contrast: more)"),r("contrast-less","@media (prefers-contrast: less)")},forcedColorsVariants:({addVariant:r})=>{r("forced-colors","@media (forced-colors: active)")}},Xe=["translate(var(--tw-translate-x), var(--tw-translate-y))","rotate(var(--tw-rotate))","skewX(var(--tw-skew-x))","skewY(var(--tw-skew-y))","scaleX(var(--tw-scale-x))","scaleY(var(--tw-scale-y))"].join(" "),nt=["var(--tw-blur)","var(--tw-brightness)","var(--tw-contrast)","var(--tw-grayscale)","var(--tw-hue-rotate)","var(--tw-invert)","var(--tw-saturate)","var(--tw-sepia)","var(--tw-drop-shadow)"].join(" "),ge=["var(--tw-backdrop-blur)","var(--tw-backdrop-brightness)","var(--tw-backdrop-contrast)","var(--tw-backdrop-grayscale)","var(--tw-backdrop-hue-rotate)","var(--tw-backdrop-invert)","var(--tw-backdrop-opacity)","var(--tw-backdrop-saturate)","var(--tw-backdrop-sepia)"].join(" "),yh={preflight:({addBase:r})=>{let e=ee.parse(`*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:theme('borderColor.DEFAULT', currentColor)}::after,::before{--tw-content:''}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:theme('fontFamily.sans', ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:theme('fontFamily.sans[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.sans[1].fontVariationSettings', normal);-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:theme('fontFamily.mono[1].fontFeatureSettings', normal);font-variation-settings:theme('fontFamily.mono[1].fontVariationSettings', normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:theme('colors.gray.4', #9ca3af)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}`);r([ee.comment({text:`! tailwindcss v${dh} | MIT License | https://tailwindcss.com`}),...e.nodes])},container:(()=>{function r(t=[]){return t.flatMap(i=>i.values.map(n=>n.min)).filter(i=>i!==void 0)}function e(t,i,n){if(typeof n=="undefined")return[];if(!(typeof n=="object"&&n!==null))return[{screen:"DEFAULT",minWidth:0,padding:n}];let s=[];n.DEFAULT&&s.push({screen:"DEFAULT",minWidth:0,padding:n.DEFAULT});for(let a of t)for(let o of i)for(let{min:l}of o.values)l===a&&s.push({minWidth:a,padding:n[o.name]});return s}return function({addComponents:t,theme:i}){let n=Rt(i("container.screens",i("screens"))),s=r(n),a=e(s,n,i("container.padding")),o=c=>{let f=a.find(d=>d.minWidth===c);return f?{paddingRight:f.padding,paddingLeft:f.padding}:{}},l=Array.from(new Set(s.slice().sort((c,f)=>parseInt(c)-parseInt(f)))).map(c=>({[`@media (min-width: ${c})`]:{".container":{"max-width":c,...o(c)}}}));t([{".container":Object.assign({width:"100%"},i("container.center",!1)?{marginRight:"auto",marginLeft:"auto"}:{},o(0))},...l])}})(),accessibility:({addUtilities:r})=>{r({".sr-only":{position:"absolute",width:"1px",height:"1px",padding:"0",margin:"-1px",overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0"},".not-sr-only":{position:"static",width:"auto",height:"auto",padding:"0",margin:"0",overflow:"visible",clip:"auto",whiteSpace:"normal"}})},pointerEvents:({addUtilities:r})=>{r({".pointer-events-none":{"pointer-events":"none"},".pointer-events-auto":{"pointer-events":"auto"}})},visibility:({addUtilities:r})=>{r({".visible":{visibility:"visible"},".invisible":{visibility:"hidden"},".collapse":{visibility:"collapse"}})},position:({addUtilities:r})=>{r({".static":{position:"static"},".fixed":{position:"fixed"},".absolute":{position:"absolute"},".relative":{position:"relative"},".sticky":{position:"sticky"}})},inset:L("inset",[["inset",["inset"]],[["inset-x",["left","right"]],["inset-y",["top","bottom"]]],[["start",["inset-inline-start"]],["end",["inset-inline-end"]],["top",["top"]],["right",["right"]],["bottom",["bottom"]],["left",["left"]]]],{supportsNegativeValues:!0}),isolation:({addUtilities:r})=>{r({".isolate":{isolation:"isolate"},".isolation-auto":{isolation:"auto"}})},zIndex:L("zIndex",[["z",["zIndex"]]],{supportsNegativeValues:!0}),order:L("order",void 0,{supportsNegativeValues:!0}),gridColumn:L("gridColumn",[["col",["gridColumn"]]]),gridColumnStart:L("gridColumnStart",[["col-start",["gridColumnStart"]]],{supportsNegativeValues:!0}),gridColumnEnd:L("gridColumnEnd",[["col-end",["gridColumnEnd"]]],{supportsNegativeValues:!0}),gridRow:L("gridRow",[["row",["gridRow"]]]),gridRowStart:L("gridRowStart",[["row-start",["gridRowStart"]]],{supportsNegativeValues:!0}),gridRowEnd:L("gridRowEnd",[["row-end",["gridRowEnd"]]],{supportsNegativeValues:!0}),float:({addUtilities:r})=>{r({".float-start":{float:"inline-start"},".float-end":{float:"inline-end"},".float-right":{float:"right"},".float-left":{float:"left"},".float-none":{float:"none"}})},clear:({addUtilities:r})=>{r({".clear-start":{clear:"inline-start"},".clear-end":{clear:"inline-end"},".clear-left":{clear:"left"},".clear-right":{clear:"right"},".clear-both":{clear:"both"},".clear-none":{clear:"none"}})},margin:L("margin",[["m",["margin"]],[["mx",["margin-left","margin-right"]],["my",["margin-top","margin-bottom"]]],[["ms",["margin-inline-start"]],["me",["margin-inline-end"]],["mt",["margin-top"]],["mr",["margin-right"]],["mb",["margin-bottom"]],["ml",["margin-left"]]]],{supportsNegativeValues:!0}),boxSizing:({addUtilities:r})=>{r({".box-border":{"box-sizing":"border-box"},".box-content":{"box-sizing":"content-box"}})},lineClamp:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"line-clamp":i=>({overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":`${i}`})},{values:t("lineClamp")}),e({".line-clamp-none":{overflow:"visible",display:"block","-webkit-box-orient":"horizontal","-webkit-line-clamp":"none"}})},display:({addUtilities:r})=>{r({".block":{display:"block"},".inline-block":{display:"inline-block"},".inline":{display:"inline"},".flex":{display:"flex"},".inline-flex":{display:"inline-flex"},".table":{display:"table"},".inline-table":{display:"inline-table"},".table-caption":{display:"table-caption"},".table-cell":{display:"table-cell"},".table-column":{display:"table-column"},".table-column-group":{display:"table-column-group"},".table-footer-group":{display:"table-footer-group"},".table-header-group":{display:"table-header-group"},".table-row-group":{display:"table-row-group"},".table-row":{display:"table-row"},".flow-root":{display:"flow-root"},".grid":{display:"grid"},".inline-grid":{display:"inline-grid"},".contents":{display:"contents"},".list-item":{display:"list-item"},".hidden":{display:"none"}})},aspectRatio:L("aspectRatio",[["aspect",["aspect-ratio"]]]),size:L("size",[["size",["width","height"]]]),height:L("height",[["h",["height"]]]),maxHeight:L("maxHeight",[["max-h",["maxHeight"]]]),minHeight:L("minHeight",[["min-h",["minHeight"]]]),width:L("width",[["w",["width"]]]),minWidth:L("minWidth",[["min-w",["minWidth"]]]),maxWidth:L("maxWidth",[["max-w",["maxWidth"]]]),flex:L("flex"),flexShrink:L("flexShrink",[["flex-shrink",["flex-shrink"]],["shrink",["flex-shrink"]]]),flexGrow:L("flexGrow",[["flex-grow",["flex-grow"]],["grow",["flex-grow"]]]),flexBasis:L("flexBasis",[["basis",["flex-basis"]]]),tableLayout:({addUtilities:r})=>{r({".table-auto":{"table-layout":"auto"},".table-fixed":{"table-layout":"fixed"}})},captionSide:({addUtilities:r})=>{r({".caption-top":{"caption-side":"top"},".caption-bottom":{"caption-side":"bottom"}})},borderCollapse:({addUtilities:r})=>{r({".border-collapse":{"border-collapse":"collapse"},".border-separate":{"border-collapse":"separate"}})},borderSpacing:({addDefaults:r,matchUtilities:e,theme:t})=>{r("border-spacing",{"--tw-border-spacing-x":0,"--tw-border-spacing-y":0}),e({"border-spacing":i=>({"--tw-border-spacing-x":i,"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-x":i=>({"--tw-border-spacing-x":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"}),"border-spacing-y":i=>({"--tw-border-spacing-y":i,"@defaults border-spacing":{},"border-spacing":"var(--tw-border-spacing-x) var(--tw-border-spacing-y)"})},{values:t("borderSpacing")})},transformOrigin:L("transformOrigin",[["origin",["transformOrigin"]]]),translate:L("translate",[[["translate-x",[["@defaults transform",{}],"--tw-translate-x",["transform",Xe]]],["translate-y",[["@defaults transform",{}],"--tw-translate-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),rotate:L("rotate",[["rotate",[["@defaults transform",{}],"--tw-rotate",["transform",Xe]]]],{supportsNegativeValues:!0}),skew:L("skew",[[["skew-x",[["@defaults transform",{}],"--tw-skew-x",["transform",Xe]]],["skew-y",[["@defaults transform",{}],"--tw-skew-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),scale:L("scale",[["scale",[["@defaults transform",{}],"--tw-scale-x","--tw-scale-y",["transform",Xe]]],[["scale-x",[["@defaults transform",{}],"--tw-scale-x",["transform",Xe]]],["scale-y",[["@defaults transform",{}],"--tw-scale-y",["transform",Xe]]]]],{supportsNegativeValues:!0}),transform:({addDefaults:r,addUtilities:e})=>{r("transform",{"--tw-translate-x":"0","--tw-translate-y":"0","--tw-rotate":"0","--tw-skew-x":"0","--tw-skew-y":"0","--tw-scale-x":"1","--tw-scale-y":"1"}),e({".transform":{"@defaults transform":{},transform:Xe},".transform-cpu":{transform:Xe},".transform-gpu":{transform:Xe.replace("translate(var(--tw-translate-x), var(--tw-translate-y))","translate3d(var(--tw-translate-x), var(--tw-translate-y), 0)")},".transform-none":{transform:"none"}})},animation:({matchUtilities:r,theme:e,config:t})=>{let i=s=>Te(t("prefix")+s),n=Object.fromEntries(Object.entries(e("keyframes")??{}).map(([s,a])=>[s,{[`@keyframes ${i(s)}`]:a}]));r({animate:s=>{let a=Mo(s);return[...a.flatMap(o=>n[o.name]),{animation:a.map(({name:o,value:l})=>o===void 0||n[o]===void 0?l:l.replace(o,i(o))).join(", ")}]}},{values:e("animation")})},cursor:L("cursor"),touchAction:({addDefaults:r,addUtilities:e})=>{r("touch-action",{"--tw-pan-x":" ","--tw-pan-y":" ","--tw-pinch-zoom":" "});let t="var(--tw-pan-x) var(--tw-pan-y) var(--tw-pinch-zoom)";e({".touch-auto":{"touch-action":"auto"},".touch-none":{"touch-action":"none"},".touch-pan-x":{"@defaults touch-action":{},"--tw-pan-x":"pan-x","touch-action":t},".touch-pan-left":{"@defaults touch-action":{},"--tw-pan-x":"pan-left","touch-action":t},".touch-pan-right":{"@defaults touch-action":{},"--tw-pan-x":"pan-right","touch-action":t},".touch-pan-y":{"@defaults touch-action":{},"--tw-pan-y":"pan-y","touch-action":t},".touch-pan-up":{"@defaults touch-action":{},"--tw-pan-y":"pan-up","touch-action":t},".touch-pan-down":{"@defaults touch-action":{},"--tw-pan-y":"pan-down","touch-action":t},".touch-pinch-zoom":{"@defaults touch-action":{},"--tw-pinch-zoom":"pinch-zoom","touch-action":t},".touch-manipulation":{"touch-action":"manipulation"}})},userSelect:({addUtilities:r})=>{r({".select-none":{"user-select":"none"},".select-text":{"user-select":"text"},".select-all":{"user-select":"all"},".select-auto":{"user-select":"auto"}})},resize:({addUtilities:r})=>{r({".resize-none":{resize:"none"},".resize-y":{resize:"vertical"},".resize-x":{resize:"horizontal"},".resize":{resize:"both"}})},scrollSnapType:({addDefaults:r,addUtilities:e})=>{r("scroll-snap-type",{"--tw-scroll-snap-strictness":"proximity"}),e({".snap-none":{"scroll-snap-type":"none"},".snap-x":{"@defaults scroll-snap-type":{},"scroll-snap-type":"x var(--tw-scroll-snap-strictness)"},".snap-y":{"@defaults scroll-snap-type":{},"scroll-snap-type":"y var(--tw-scroll-snap-strictness)"},".snap-both":{"@defaults scroll-snap-type":{},"scroll-snap-type":"both var(--tw-scroll-snap-strictness)"},".snap-mandatory":{"--tw-scroll-snap-strictness":"mandatory"},".snap-proximity":{"--tw-scroll-snap-strictness":"proximity"}})},scrollSnapAlign:({addUtilities:r})=>{r({".snap-start":{"scroll-snap-align":"start"},".snap-end":{"scroll-snap-align":"end"},".snap-center":{"scroll-snap-align":"center"},".snap-align-none":{"scroll-snap-align":"none"}})},scrollSnapStop:({addUtilities:r})=>{r({".snap-normal":{"scroll-snap-stop":"normal"},".snap-always":{"scroll-snap-stop":"always"}})},scrollMargin:L("scrollMargin",[["scroll-m",["scroll-margin"]],[["scroll-mx",["scroll-margin-left","scroll-margin-right"]],["scroll-my",["scroll-margin-top","scroll-margin-bottom"]]],[["scroll-ms",["scroll-margin-inline-start"]],["scroll-me",["scroll-margin-inline-end"]],["scroll-mt",["scroll-margin-top"]],["scroll-mr",["scroll-margin-right"]],["scroll-mb",["scroll-margin-bottom"]],["scroll-ml",["scroll-margin-left"]]]],{supportsNegativeValues:!0}),scrollPadding:L("scrollPadding",[["scroll-p",["scroll-padding"]],[["scroll-px",["scroll-padding-left","scroll-padding-right"]],["scroll-py",["scroll-padding-top","scroll-padding-bottom"]]],[["scroll-ps",["scroll-padding-inline-start"]],["scroll-pe",["scroll-padding-inline-end"]],["scroll-pt",["scroll-padding-top"]],["scroll-pr",["scroll-padding-right"]],["scroll-pb",["scroll-padding-bottom"]],["scroll-pl",["scroll-padding-left"]]]]),listStylePosition:({addUtilities:r})=>{r({".list-inside":{"list-style-position":"inside"},".list-outside":{"list-style-position":"outside"}})},listStyleType:L("listStyleType",[["list",["listStyleType"]]]),listStyleImage:L("listStyleImage",[["list-image",["listStyleImage"]]]),appearance:({addUtilities:r})=>{r({".appearance-none":{appearance:"none"},".appearance-auto":{appearance:"auto"}})},columns:L("columns",[["columns",["columns"]]]),breakBefore:({addUtilities:r})=>{r({".break-before-auto":{"break-before":"auto"},".break-before-avoid":{"break-before":"avoid"},".break-before-all":{"break-before":"all"},".break-before-avoid-page":{"break-before":"avoid-page"},".break-before-page":{"break-before":"page"},".break-before-left":{"break-before":"left"},".break-before-right":{"break-before":"right"},".break-before-column":{"break-before":"column"}})},breakInside:({addUtilities:r})=>{r({".break-inside-auto":{"break-inside":"auto"},".break-inside-avoid":{"break-inside":"avoid"},".break-inside-avoid-page":{"break-inside":"avoid-page"},".break-inside-avoid-column":{"break-inside":"avoid-column"}})},breakAfter:({addUtilities:r})=>{r({".break-after-auto":{"break-after":"auto"},".break-after-avoid":{"break-after":"avoid"},".break-after-all":{"break-after":"all"},".break-after-avoid-page":{"break-after":"avoid-page"},".break-after-page":{"break-after":"page"},".break-after-left":{"break-after":"left"},".break-after-right":{"break-after":"right"},".break-after-column":{"break-after":"column"}})},gridAutoColumns:L("gridAutoColumns",[["auto-cols",["gridAutoColumns"]]]),gridAutoFlow:({addUtilities:r})=>{r({".grid-flow-row":{gridAutoFlow:"row"},".grid-flow-col":{gridAutoFlow:"column"},".grid-flow-dense":{gridAutoFlow:"dense"},".grid-flow-row-dense":{gridAutoFlow:"row dense"},".grid-flow-col-dense":{gridAutoFlow:"column dense"}})},gridAutoRows:L("gridAutoRows",[["auto-rows",["gridAutoRows"]]]),gridTemplateColumns:L("gridTemplateColumns",[["grid-cols",["gridTemplateColumns"]]]),gridTemplateRows:L("gridTemplateRows",[["grid-rows",["gridTemplateRows"]]]),flexDirection:({addUtilities:r})=>{r({".flex-row":{"flex-direction":"row"},".flex-row-reverse":{"flex-direction":"row-reverse"},".flex-col":{"flex-direction":"column"},".flex-col-reverse":{"flex-direction":"column-reverse"}})},flexWrap:({addUtilities:r})=>{r({".flex-wrap":{"flex-wrap":"wrap"},".flex-wrap-reverse":{"flex-wrap":"wrap-reverse"},".flex-nowrap":{"flex-wrap":"nowrap"}})},placeContent:({addUtilities:r})=>{r({".place-content-center":{"place-content":"center"},".place-content-start":{"place-content":"start"},".place-content-end":{"place-content":"end"},".place-content-between":{"place-content":"space-between"},".place-content-around":{"place-content":"space-around"},".place-content-evenly":{"place-content":"space-evenly"},".place-content-baseline":{"place-content":"baseline"},".place-content-stretch":{"place-content":"stretch"}})},placeItems:({addUtilities:r})=>{r({".place-items-start":{"place-items":"start"},".place-items-end":{"place-items":"end"},".place-items-center":{"place-items":"center"},".place-items-baseline":{"place-items":"baseline"},".place-items-stretch":{"place-items":"stretch"}})},alignContent:({addUtilities:r})=>{r({".content-normal":{"align-content":"normal"},".content-center":{"align-content":"center"},".content-start":{"align-content":"flex-start"},".content-end":{"align-content":"flex-end"},".content-between":{"align-content":"space-between"},".content-around":{"align-content":"space-around"},".content-evenly":{"align-content":"space-evenly"},".content-baseline":{"align-content":"baseline"},".content-stretch":{"align-content":"stretch"}})},alignItems:({addUtilities:r})=>{r({".items-start":{"align-items":"flex-start"},".items-end":{"align-items":"flex-end"},".items-center":{"align-items":"center"},".items-baseline":{"align-items":"baseline"},".items-stretch":{"align-items":"stretch"}})},justifyContent:({addUtilities:r})=>{r({".justify-normal":{"justify-content":"normal"},".justify-start":{"justify-content":"flex-start"},".justify-end":{"justify-content":"flex-end"},".justify-center":{"justify-content":"center"},".justify-between":{"justify-content":"space-between"},".justify-around":{"justify-content":"space-around"},".justify-evenly":{"justify-content":"space-evenly"},".justify-stretch":{"justify-content":"stretch"}})},justifyItems:({addUtilities:r})=>{r({".justify-items-start":{"justify-items":"start"},".justify-items-end":{"justify-items":"end"},".justify-items-center":{"justify-items":"center"},".justify-items-stretch":{"justify-items":"stretch"}})},gap:L("gap",[["gap",["gap"]],[["gap-x",["columnGap"]],["gap-y",["rowGap"]]]]),space:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"space-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"0","margin-right":`calc(${i} * var(--tw-space-x-reverse))`,"margin-left":`calc(${i} * calc(1 - var(--tw-space-x-reverse)))`}}),"space-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"0","margin-top":`calc(${i} * calc(1 - var(--tw-space-y-reverse)))`,"margin-bottom":`calc(${i} * var(--tw-space-y-reverse))`}})},{values:t("space"),supportsNegativeValues:!0}),e({".space-y-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-y-reverse":"1"},".space-x-reverse > :not([hidden]) ~ :not([hidden])":{"--tw-space-x-reverse":"1"}})},divideWidth:({matchUtilities:r,addUtilities:e,theme:t})=>{r({"divide-x":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"0","border-right-width":`calc(${i} * var(--tw-divide-x-reverse))`,"border-left-width":`calc(${i} * calc(1 - var(--tw-divide-x-reverse)))`}}),"divide-y":i=>(i=i==="0"?"0px":i,{"& > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"0","border-top-width":`calc(${i} * calc(1 - var(--tw-divide-y-reverse)))`,"border-bottom-width":`calc(${i} * var(--tw-divide-y-reverse))`}})},{values:t("divideWidth"),type:["line-width","length","any"]}),e({".divide-y-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-y-reverse":"1"},".divide-x-reverse > :not([hidden]) ~ :not([hidden])":{"@defaults border-width":{},"--tw-divide-x-reverse":"1"}})},divideStyle:({addUtilities:r})=>{r({".divide-solid > :not([hidden]) ~ :not([hidden])":{"border-style":"solid"},".divide-dashed > :not([hidden]) ~ :not([hidden])":{"border-style":"dashed"},".divide-dotted > :not([hidden]) ~ :not([hidden])":{"border-style":"dotted"},".divide-double > :not([hidden]) ~ :not([hidden])":{"border-style":"double"},".divide-none > :not([hidden]) ~ :not([hidden])":{"border-style":"none"}})},divideColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({divide:i=>t("divideOpacity")?{["& > :not([hidden]) ~ :not([hidden])"]:Ae({color:i,property:"border-color",variable:"--tw-divide-opacity"})}:{["& > :not([hidden]) ~ :not([hidden])"]:{"border-color":X(i)}}},{values:(({DEFAULT:i,...n})=>n)(xe(e("divideColor"))),type:["color","any"]})},divideOpacity:({matchUtilities:r,theme:e})=>{r({"divide-opacity":t=>({["& > :not([hidden]) ~ :not([hidden])"]:{"--tw-divide-opacity":t}})},{values:e("divideOpacity")})},placeSelf:({addUtilities:r})=>{r({".place-self-auto":{"place-self":"auto"},".place-self-start":{"place-self":"start"},".place-self-end":{"place-self":"end"},".place-self-center":{"place-self":"center"},".place-self-stretch":{"place-self":"stretch"}})},alignSelf:({addUtilities:r})=>{r({".self-auto":{"align-self":"auto"},".self-start":{"align-self":"flex-start"},".self-end":{"align-self":"flex-end"},".self-center":{"align-self":"center"},".self-stretch":{"align-self":"stretch"},".self-baseline":{"align-self":"baseline"}})},justifySelf:({addUtilities:r})=>{r({".justify-self-auto":{"justify-self":"auto"},".justify-self-start":{"justify-self":"start"},".justify-self-end":{"justify-self":"end"},".justify-self-center":{"justify-self":"center"},".justify-self-stretch":{"justify-self":"stretch"}})},overflow:({addUtilities:r})=>{r({".overflow-auto":{overflow:"auto"},".overflow-hidden":{overflow:"hidden"},".overflow-clip":{overflow:"clip"},".overflow-visible":{overflow:"visible"},".overflow-scroll":{overflow:"scroll"},".overflow-x-auto":{"overflow-x":"auto"},".overflow-y-auto":{"overflow-y":"auto"},".overflow-x-hidden":{"overflow-x":"hidden"},".overflow-y-hidden":{"overflow-y":"hidden"},".overflow-x-clip":{"overflow-x":"clip"},".overflow-y-clip":{"overflow-y":"clip"},".overflow-x-visible":{"overflow-x":"visible"},".overflow-y-visible":{"overflow-y":"visible"},".overflow-x-scroll":{"overflow-x":"scroll"},".overflow-y-scroll":{"overflow-y":"scroll"}})},overscrollBehavior:({addUtilities:r})=>{r({".overscroll-auto":{"overscroll-behavior":"auto"},".overscroll-contain":{"overscroll-behavior":"contain"},".overscroll-none":{"overscroll-behavior":"none"},".overscroll-y-auto":{"overscroll-behavior-y":"auto"},".overscroll-y-contain":{"overscroll-behavior-y":"contain"},".overscroll-y-none":{"overscroll-behavior-y":"none"},".overscroll-x-auto":{"overscroll-behavior-x":"auto"},".overscroll-x-contain":{"overscroll-behavior-x":"contain"},".overscroll-x-none":{"overscroll-behavior-x":"none"}})},scrollBehavior:({addUtilities:r})=>{r({".scroll-auto":{"scroll-behavior":"auto"},".scroll-smooth":{"scroll-behavior":"smooth"}})},textOverflow:({addUtilities:r})=>{r({".truncate":{overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"},".overflow-ellipsis":{"text-overflow":"ellipsis"},".text-ellipsis":{"text-overflow":"ellipsis"},".text-clip":{"text-overflow":"clip"}})},hyphens:({addUtilities:r})=>{r({".hyphens-none":{hyphens:"none"},".hyphens-manual":{hyphens:"manual"},".hyphens-auto":{hyphens:"auto"}})},whitespace:({addUtilities:r})=>{r({".whitespace-normal":{"white-space":"normal"},".whitespace-nowrap":{"white-space":"nowrap"},".whitespace-pre":{"white-space":"pre"},".whitespace-pre-line":{"white-space":"pre-line"},".whitespace-pre-wrap":{"white-space":"pre-wrap"},".whitespace-break-spaces":{"white-space":"break-spaces"}})},textWrap:({addUtilities:r})=>{r({".text-wrap":{"text-wrap":"wrap"},".text-nowrap":{"text-wrap":"nowrap"},".text-balance":{"text-wrap":"balance"},".text-pretty":{"text-wrap":"pretty"}})},wordBreak:({addUtilities:r})=>{r({".break-normal":{"overflow-wrap":"normal","word-break":"normal"},".break-words":{"overflow-wrap":"break-word"},".break-all":{"word-break":"break-all"},".break-keep":{"word-break":"keep-all"}})},borderRadius:L("borderRadius",[["rounded",["border-radius"]],[["rounded-s",["border-start-start-radius","border-end-start-radius"]],["rounded-e",["border-start-end-radius","border-end-end-radius"]],["rounded-t",["border-top-left-radius","border-top-right-radius"]],["rounded-r",["border-top-right-radius","border-bottom-right-radius"]],["rounded-b",["border-bottom-right-radius","border-bottom-left-radius"]],["rounded-l",["border-top-left-radius","border-bottom-left-radius"]]],[["rounded-ss",["border-start-start-radius"]],["rounded-se",["border-start-end-radius"]],["rounded-ee",["border-end-end-radius"]],["rounded-es",["border-end-start-radius"]],["rounded-tl",["border-top-left-radius"]],["rounded-tr",["border-top-right-radius"]],["rounded-br",["border-bottom-right-radius"]],["rounded-bl",["border-bottom-left-radius"]]]]),borderWidth:L("borderWidth",[["border",[["@defaults border-width",{}],"border-width"]],[["border-x",[["@defaults border-width",{}],"border-left-width","border-right-width"]],["border-y",[["@defaults border-width",{}],"border-top-width","border-bottom-width"]]],[["border-s",[["@defaults border-width",{}],"border-inline-start-width"]],["border-e",[["@defaults border-width",{}],"border-inline-end-width"]],["border-t",[["@defaults border-width",{}],"border-top-width"]],["border-r",[["@defaults border-width",{}],"border-right-width"]],["border-b",[["@defaults border-width",{}],"border-bottom-width"]],["border-l",[["@defaults border-width",{}],"border-left-width"]]]],{type:["line-width","length"]}),borderStyle:({addUtilities:r})=>{r({".border-solid":{"border-style":"solid"},".border-dashed":{"border-style":"dashed"},".border-dotted":{"border-style":"dotted"},".border-double":{"border-style":"double"},".border-hidden":{"border-style":"hidden"},".border-none":{"border-style":"none"}})},borderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({border:i=>t("borderOpacity")?Ae({color:i,property:"border-color",variable:"--tw-border-opacity"}):{"border-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]}),r({"border-x":i=>t("borderOpacity")?Ae({color:i,property:["border-left-color","border-right-color"],variable:"--tw-border-opacity"}):{"border-left-color":X(i),"border-right-color":X(i)},"border-y":i=>t("borderOpacity")?Ae({color:i,property:["border-top-color","border-bottom-color"],variable:"--tw-border-opacity"}):{"border-top-color":X(i),"border-bottom-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]}),r({"border-s":i=>t("borderOpacity")?Ae({color:i,property:"border-inline-start-color",variable:"--tw-border-opacity"}):{"border-inline-start-color":X(i)},"border-e":i=>t("borderOpacity")?Ae({color:i,property:"border-inline-end-color",variable:"--tw-border-opacity"}):{"border-inline-end-color":X(i)},"border-t":i=>t("borderOpacity")?Ae({color:i,property:"border-top-color",variable:"--tw-border-opacity"}):{"border-top-color":X(i)},"border-r":i=>t("borderOpacity")?Ae({color:i,property:"border-right-color",variable:"--tw-border-opacity"}):{"border-right-color":X(i)},"border-b":i=>t("borderOpacity")?Ae({color:i,property:"border-bottom-color",variable:"--tw-border-opacity"}):{"border-bottom-color":X(i)},"border-l":i=>t("borderOpacity")?Ae({color:i,property:"border-left-color",variable:"--tw-border-opacity"}):{"border-left-color":X(i)}},{values:(({DEFAULT:i,...n})=>n)(xe(e("borderColor"))),type:["color","any"]})},borderOpacity:L("borderOpacity",[["border-opacity",["--tw-border-opacity"]]]),backgroundColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({bg:i=>t("backgroundOpacity")?Ae({color:i,property:"background-color",variable:"--tw-bg-opacity"}):{"background-color":X(i)}},{values:xe(e("backgroundColor")),type:["color","any"]})},backgroundOpacity:L("backgroundOpacity",[["bg-opacity",["--tw-bg-opacity"]]]),backgroundImage:L("backgroundImage",[["bg",["background-image"]]],{type:["lookup","image","url"]}),gradientColorStops:(()=>{function r(e){return Je(e,0,"rgb(255 255 255 / 0)")}return function({matchUtilities:e,theme:t,addDefaults:i}){i("gradient-color-stops",{"--tw-gradient-from-position":" ","--tw-gradient-via-position":" ","--tw-gradient-to-position":" "});let n={values:xe(t("gradientColorStops")),type:["color","any"]},s={values:t("gradientColorStopPositions"),type:["length","percentage"]};e({from:a=>{let o=r(a);return{"@defaults gradient-color-stops":{},"--tw-gradient-from":`${X(a)} var(--tw-gradient-from-position)`,"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":"var(--tw-gradient-from), var(--tw-gradient-to)"}}},n),e({from:a=>({"--tw-gradient-from-position":a})},s),e({via:a=>{let o=r(a);return{"@defaults gradient-color-stops":{},"--tw-gradient-to":`${o} var(--tw-gradient-to-position)`,"--tw-gradient-stops":`var(--tw-gradient-from), ${X(a)} var(--tw-gradient-via-position), var(--tw-gradient-to)`}}},n),e({via:a=>({"--tw-gradient-via-position":a})},s),e({to:a=>({"@defaults gradient-color-stops":{},"--tw-gradient-to":`${X(a)} var(--tw-gradient-to-position)`})},n),e({to:a=>({"--tw-gradient-to-position":a})},s)}})(),boxDecorationBreak:({addUtilities:r})=>{r({".decoration-slice":{"box-decoration-break":"slice"},".decoration-clone":{"box-decoration-break":"clone"},".box-decoration-slice":{"box-decoration-break":"slice"},".box-decoration-clone":{"box-decoration-break":"clone"}})},backgroundSize:L("backgroundSize",[["bg",["background-size"]]],{type:["lookup","length","percentage","size"]}),backgroundAttachment:({addUtilities:r})=>{r({".bg-fixed":{"background-attachment":"fixed"},".bg-local":{"background-attachment":"local"},".bg-scroll":{"background-attachment":"scroll"}})},backgroundClip:({addUtilities:r})=>{r({".bg-clip-border":{"background-clip":"border-box"},".bg-clip-padding":{"background-clip":"padding-box"},".bg-clip-content":{"background-clip":"content-box"},".bg-clip-text":{"background-clip":"text"}})},backgroundPosition:L("backgroundPosition",[["bg",["background-position"]]],{type:["lookup",["position",{preferOnConflict:!0}]]}),backgroundRepeat:({addUtilities:r})=>{r({".bg-repeat":{"background-repeat":"repeat"},".bg-no-repeat":{"background-repeat":"no-repeat"},".bg-repeat-x":{"background-repeat":"repeat-x"},".bg-repeat-y":{"background-repeat":"repeat-y"},".bg-repeat-round":{"background-repeat":"round"},".bg-repeat-space":{"background-repeat":"space"}})},backgroundOrigin:({addUtilities:r})=>{r({".bg-origin-border":{"background-origin":"border-box"},".bg-origin-padding":{"background-origin":"padding-box"},".bg-origin-content":{"background-origin":"content-box"}})},fill:({matchUtilities:r,theme:e})=>{r({fill:t=>({fill:X(t)})},{values:xe(e("fill")),type:["color","any"]})},stroke:({matchUtilities:r,theme:e})=>{r({stroke:t=>({stroke:X(t)})},{values:xe(e("stroke")),type:["color","url","any"]})},strokeWidth:L("strokeWidth",[["stroke",["stroke-width"]]],{type:["length","number","percentage"]}),objectFit:({addUtilities:r})=>{r({".object-contain":{"object-fit":"contain"},".object-cover":{"object-fit":"cover"},".object-fill":{"object-fit":"fill"},".object-none":{"object-fit":"none"},".object-scale-down":{"object-fit":"scale-down"}})},objectPosition:L("objectPosition",[["object",["object-position"]]]),padding:L("padding",[["p",["padding"]],[["px",["padding-left","padding-right"]],["py",["padding-top","padding-bottom"]]],[["ps",["padding-inline-start"]],["pe",["padding-inline-end"]],["pt",["padding-top"]],["pr",["padding-right"]],["pb",["padding-bottom"]],["pl",["padding-left"]]]]),textAlign:({addUtilities:r})=>{r({".text-left":{"text-align":"left"},".text-center":{"text-align":"center"},".text-right":{"text-align":"right"},".text-justify":{"text-align":"justify"},".text-start":{"text-align":"start"},".text-end":{"text-align":"end"}})},textIndent:L("textIndent",[["indent",["text-indent"]]],{supportsNegativeValues:!0}),verticalAlign:({addUtilities:r,matchUtilities:e})=>{r({".align-baseline":{"vertical-align":"baseline"},".align-top":{"vertical-align":"top"},".align-middle":{"vertical-align":"middle"},".align-bottom":{"vertical-align":"bottom"},".align-text-top":{"vertical-align":"text-top"},".align-text-bottom":{"vertical-align":"text-bottom"},".align-sub":{"vertical-align":"sub"},".align-super":{"vertical-align":"super"}}),e({align:t=>({"vertical-align":t})})},fontFamily:({matchUtilities:r,theme:e})=>{r({font:t=>{let[i,n={}]=Array.isArray(t)&&ke(t[1])?t:[t],{fontFeatureSettings:s,fontVariationSettings:a}=n;return{"font-family":Array.isArray(i)?i.join(", "):i,...s===void 0?{}:{"font-feature-settings":s},...a===void 0?{}:{"font-variation-settings":a}}}},{values:e("fontFamily"),type:["lookup","generic-name","family-name"]})},fontSize:({matchUtilities:r,theme:e})=>{r({text:(t,{modifier:i})=>{let[n,s]=Array.isArray(t)?t:[t];if(i)return{"font-size":n,"line-height":i};let{lineHeight:a,letterSpacing:o,fontWeight:l}=ke(s)?s:{lineHeight:s};return{"font-size":n,...a===void 0?{}:{"line-height":a},...o===void 0?{}:{"letter-spacing":o},...l===void 0?{}:{"font-weight":l}}}},{values:e("fontSize"),modifiers:e("lineHeight"),type:["absolute-size","relative-size","length","percentage"]})},fontWeight:L("fontWeight",[["font",["fontWeight"]]],{type:["lookup","number","any"]}),textTransform:({addUtilities:r})=>{r({".uppercase":{"text-transform":"uppercase"},".lowercase":{"text-transform":"lowercase"},".capitalize":{"text-transform":"capitalize"},".normal-case":{"text-transform":"none"}})},fontStyle:({addUtilities:r})=>{r({".italic":{"font-style":"italic"},".not-italic":{"font-style":"normal"}})},fontVariantNumeric:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)";r("font-variant-numeric",{"--tw-ordinal":" ","--tw-slashed-zero":" ","--tw-numeric-figure":" ","--tw-numeric-spacing":" ","--tw-numeric-fraction":" "}),e({".normal-nums":{"font-variant-numeric":"normal"},".ordinal":{"@defaults font-variant-numeric":{},"--tw-ordinal":"ordinal","font-variant-numeric":t},".slashed-zero":{"@defaults font-variant-numeric":{},"--tw-slashed-zero":"slashed-zero","font-variant-numeric":t},".lining-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"lining-nums","font-variant-numeric":t},".oldstyle-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-figure":"oldstyle-nums","font-variant-numeric":t},".proportional-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"proportional-nums","font-variant-numeric":t},".tabular-nums":{"@defaults font-variant-numeric":{},"--tw-numeric-spacing":"tabular-nums","font-variant-numeric":t},".diagonal-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"diagonal-fractions","font-variant-numeric":t},".stacked-fractions":{"@defaults font-variant-numeric":{},"--tw-numeric-fraction":"stacked-fractions","font-variant-numeric":t}})},lineHeight:L("lineHeight",[["leading",["lineHeight"]]]),letterSpacing:L("letterSpacing",[["tracking",["letterSpacing"]]],{supportsNegativeValues:!0}),textColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({text:i=>t("textOpacity")?Ae({color:i,property:"color",variable:"--tw-text-opacity"}):{color:X(i)}},{values:xe(e("textColor")),type:["color","any"]})},textOpacity:L("textOpacity",[["text-opacity",["--tw-text-opacity"]]]),textDecoration:({addUtilities:r})=>{r({".underline":{"text-decoration-line":"underline"},".overline":{"text-decoration-line":"overline"},".line-through":{"text-decoration-line":"line-through"},".no-underline":{"text-decoration-line":"none"}})},textDecorationColor:({matchUtilities:r,theme:e})=>{r({decoration:t=>({"text-decoration-color":X(t)})},{values:xe(e("textDecorationColor")),type:["color","any"]})},textDecorationStyle:({addUtilities:r})=>{r({".decoration-solid":{"text-decoration-style":"solid"},".decoration-double":{"text-decoration-style":"double"},".decoration-dotted":{"text-decoration-style":"dotted"},".decoration-dashed":{"text-decoration-style":"dashed"},".decoration-wavy":{"text-decoration-style":"wavy"}})},textDecorationThickness:L("textDecorationThickness",[["decoration",["text-decoration-thickness"]]],{type:["length","percentage"]}),textUnderlineOffset:L("textUnderlineOffset",[["underline-offset",["text-underline-offset"]]],{type:["length","percentage","any"]}),fontSmoothing:({addUtilities:r})=>{r({".antialiased":{"-webkit-font-smoothing":"antialiased","-moz-osx-font-smoothing":"grayscale"},".subpixel-antialiased":{"-webkit-font-smoothing":"auto","-moz-osx-font-smoothing":"auto"}})},placeholderColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({placeholder:i=>t("placeholderOpacity")?{"&::placeholder":Ae({color:i,property:"color",variable:"--tw-placeholder-opacity"})}:{"&::placeholder":{color:X(i)}}},{values:xe(e("placeholderColor")),type:["color","any"]})},placeholderOpacity:({matchUtilities:r,theme:e})=>{r({"placeholder-opacity":t=>({["&::placeholder"]:{"--tw-placeholder-opacity":t}})},{values:e("placeholderOpacity")})},caretColor:({matchUtilities:r,theme:e})=>{r({caret:t=>({"caret-color":X(t)})},{values:xe(e("caretColor")),type:["color","any"]})},accentColor:({matchUtilities:r,theme:e})=>{r({accent:t=>({"accent-color":X(t)})},{values:xe(e("accentColor")),type:["color","any"]})},opacity:L("opacity",[["opacity",["opacity"]]]),backgroundBlendMode:({addUtilities:r})=>{r({".bg-blend-normal":{"background-blend-mode":"normal"},".bg-blend-multiply":{"background-blend-mode":"multiply"},".bg-blend-screen":{"background-blend-mode":"screen"},".bg-blend-overlay":{"background-blend-mode":"overlay"},".bg-blend-darken":{"background-blend-mode":"darken"},".bg-blend-lighten":{"background-blend-mode":"lighten"},".bg-blend-color-dodge":{"background-blend-mode":"color-dodge"},".bg-blend-color-burn":{"background-blend-mode":"color-burn"},".bg-blend-hard-light":{"background-blend-mode":"hard-light"},".bg-blend-soft-light":{"background-blend-mode":"soft-light"},".bg-blend-difference":{"background-blend-mode":"difference"},".bg-blend-exclusion":{"background-blend-mode":"exclusion"},".bg-blend-hue":{"background-blend-mode":"hue"},".bg-blend-saturation":{"background-blend-mode":"saturation"},".bg-blend-color":{"background-blend-mode":"color"},".bg-blend-luminosity":{"background-blend-mode":"luminosity"}})},mixBlendMode:({addUtilities:r})=>{r({".mix-blend-normal":{"mix-blend-mode":"normal"},".mix-blend-multiply":{"mix-blend-mode":"multiply"},".mix-blend-screen":{"mix-blend-mode":"screen"},".mix-blend-overlay":{"mix-blend-mode":"overlay"},".mix-blend-darken":{"mix-blend-mode":"darken"},".mix-blend-lighten":{"mix-blend-mode":"lighten"},".mix-blend-color-dodge":{"mix-blend-mode":"color-dodge"},".mix-blend-color-burn":{"mix-blend-mode":"color-burn"},".mix-blend-hard-light":{"mix-blend-mode":"hard-light"},".mix-blend-soft-light":{"mix-blend-mode":"soft-light"},".mix-blend-difference":{"mix-blend-mode":"difference"},".mix-blend-exclusion":{"mix-blend-mode":"exclusion"},".mix-blend-hue":{"mix-blend-mode":"hue"},".mix-blend-saturation":{"mix-blend-mode":"saturation"},".mix-blend-color":{"mix-blend-mode":"color"},".mix-blend-luminosity":{"mix-blend-mode":"luminosity"},".mix-blend-plus-darker":{"mix-blend-mode":"plus-darker"},".mix-blend-plus-lighter":{"mix-blend-mode":"plus-lighter"}})},boxShadow:(()=>{let r=mt("boxShadow"),e=["var(--tw-ring-offset-shadow, 0 0 #0000)","var(--tw-ring-shadow, 0 0 #0000)","var(--tw-shadow)"].join(", ");return function({matchUtilities:t,addDefaults:i,theme:n}){i("box-shadow",{"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),t({shadow:s=>{s=r(s);let a=en(s);for(let o of a)!o.valid||(o.color="var(--tw-shadow-color)");return{"@defaults box-shadow":{},"--tw-shadow":s==="none"?"0 0 #0000":s,"--tw-shadow-colored":s==="none"?"0 0 #0000":Lf(a),"box-shadow":e}}},{values:n("boxShadow"),type:["shadow"]})}})(),boxShadowColor:({matchUtilities:r,theme:e})=>{r({shadow:t=>({"--tw-shadow-color":X(t),"--tw-shadow":"var(--tw-shadow-colored)"})},{values:xe(e("boxShadowColor")),type:["color","any"]})},outlineStyle:({addUtilities:r})=>{r({".outline-none":{outline:"2px solid transparent","outline-offset":"2px"},".outline":{"outline-style":"solid"},".outline-dashed":{"outline-style":"dashed"},".outline-dotted":{"outline-style":"dotted"},".outline-double":{"outline-style":"double"}})},outlineWidth:L("outlineWidth",[["outline",["outline-width"]]],{type:["length","number","percentage"]}),outlineOffset:L("outlineOffset",[["outline-offset",["outline-offset"]]],{type:["length","number","percentage","any"],supportsNegativeValues:!0}),outlineColor:({matchUtilities:r,theme:e})=>{r({outline:t=>({"outline-color":X(t)})},{values:xe(e("outlineColor")),type:["color","any"]})},ringWidth:({matchUtilities:r,addDefaults:e,addUtilities:t,theme:i,config:n})=>{let s=(()=>{if(we(n(),"respectDefaultRingColorOpacity"))return i("ringColor.DEFAULT");let a=i("ringOpacity.DEFAULT","0.5");return i("ringColor")?.DEFAULT?Je(i("ringColor")?.DEFAULT,a,`rgb(147 197 253 / ${a})`):`rgb(147 197 253 / ${a})`})();e("ring-width",{"--tw-ring-inset":" ","--tw-ring-offset-width":i("ringOffsetWidth.DEFAULT","0px"),"--tw-ring-offset-color":i("ringOffsetColor.DEFAULT","#fff"),"--tw-ring-color":s,"--tw-ring-offset-shadow":"0 0 #0000","--tw-ring-shadow":"0 0 #0000","--tw-shadow":"0 0 #0000","--tw-shadow-colored":"0 0 #0000"}),r({ring:a=>({"@defaults ring-width":{},"--tw-ring-offset-shadow":"var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)","--tw-ring-shadow":`var(--tw-ring-inset) 0 0 0 calc(${a} + var(--tw-ring-offset-width)) var(--tw-ring-color)`,"box-shadow":["var(--tw-ring-offset-shadow)","var(--tw-ring-shadow)","var(--tw-shadow, 0 0 #0000)"].join(", ")})},{values:i("ringWidth"),type:"length"}),t({".ring-inset":{"@defaults ring-width":{},"--tw-ring-inset":"inset"}})},ringColor:({matchUtilities:r,theme:e,corePlugins:t})=>{r({ring:i=>t("ringOpacity")?Ae({color:i,property:"--tw-ring-color",variable:"--tw-ring-opacity"}):{"--tw-ring-color":X(i)}},{values:Object.fromEntries(Object.entries(xe(e("ringColor"))).filter(([i])=>i!=="DEFAULT")),type:["color","any"]})},ringOpacity:r=>{let{config:e}=r;return L("ringOpacity",[["ring-opacity",["--tw-ring-opacity"]]],{filterDefault:!we(e(),"respectDefaultRingColorOpacity")})(r)},ringOffsetWidth:L("ringOffsetWidth",[["ring-offset",["--tw-ring-offset-width"]]],{type:"length"}),ringOffsetColor:({matchUtilities:r,theme:e})=>{r({"ring-offset":t=>({"--tw-ring-offset-color":X(t)})},{values:xe(e("ringOffsetColor")),type:["color","any"]})},blur:({matchUtilities:r,theme:e})=>{r({blur:t=>({"--tw-blur":t.trim()===""?" ":`blur(${t})`,"@defaults filter":{},filter:nt})},{values:e("blur")})},brightness:({matchUtilities:r,theme:e})=>{r({brightness:t=>({"--tw-brightness":`brightness(${t})`,"@defaults filter":{},filter:nt})},{values:e("brightness")})},contrast:({matchUtilities:r,theme:e})=>{r({contrast:t=>({"--tw-contrast":`contrast(${t})`,"@defaults filter":{},filter:nt})},{values:e("contrast")})},dropShadow:({matchUtilities:r,theme:e})=>{r({"drop-shadow":t=>({"--tw-drop-shadow":Array.isArray(t)?t.map(i=>`drop-shadow(${i})`).join(" "):`drop-shadow(${t})`,"@defaults filter":{},filter:nt})},{values:e("dropShadow")})},grayscale:({matchUtilities:r,theme:e})=>{r({grayscale:t=>({"--tw-grayscale":`grayscale(${t})`,"@defaults filter":{},filter:nt})},{values:e("grayscale")})},hueRotate:({matchUtilities:r,theme:e})=>{r({"hue-rotate":t=>({"--tw-hue-rotate":`hue-rotate(${t})`,"@defaults filter":{},filter:nt})},{values:e("hueRotate"),supportsNegativeValues:!0})},invert:({matchUtilities:r,theme:e})=>{r({invert:t=>({"--tw-invert":`invert(${t})`,"@defaults filter":{},filter:nt})},{values:e("invert")})},saturate:({matchUtilities:r,theme:e})=>{r({saturate:t=>({"--tw-saturate":`saturate(${t})`,"@defaults filter":{},filter:nt})},{values:e("saturate")})},sepia:({matchUtilities:r,theme:e})=>{r({sepia:t=>({"--tw-sepia":`sepia(${t})`,"@defaults filter":{},filter:nt})},{values:e("sepia")})},filter:({addDefaults:r,addUtilities:e})=>{r("filter",{"--tw-blur":" ","--tw-brightness":" ","--tw-contrast":" ","--tw-grayscale":" ","--tw-hue-rotate":" ","--tw-invert":" ","--tw-saturate":" ","--tw-sepia":" ","--tw-drop-shadow":" "}),e({".filter":{"@defaults filter":{},filter:nt},".filter-none":{filter:"none"}})},backdropBlur:({matchUtilities:r,theme:e})=>{r({"backdrop-blur":t=>({"--tw-backdrop-blur":t.trim()===""?" ":`blur(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropBlur")})},backdropBrightness:({matchUtilities:r,theme:e})=>{r({"backdrop-brightness":t=>({"--tw-backdrop-brightness":`brightness(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropBrightness")})},backdropContrast:({matchUtilities:r,theme:e})=>{r({"backdrop-contrast":t=>({"--tw-backdrop-contrast":`contrast(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropContrast")})},backdropGrayscale:({matchUtilities:r,theme:e})=>{r({"backdrop-grayscale":t=>({"--tw-backdrop-grayscale":`grayscale(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropGrayscale")})},backdropHueRotate:({matchUtilities:r,theme:e})=>{r({"backdrop-hue-rotate":t=>({"--tw-backdrop-hue-rotate":`hue-rotate(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropHueRotate"),supportsNegativeValues:!0})},backdropInvert:({matchUtilities:r,theme:e})=>{r({"backdrop-invert":t=>({"--tw-backdrop-invert":`invert(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropInvert")})},backdropOpacity:({matchUtilities:r,theme:e})=>{r({"backdrop-opacity":t=>({"--tw-backdrop-opacity":`opacity(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropOpacity")})},backdropSaturate:({matchUtilities:r,theme:e})=>{r({"backdrop-saturate":t=>({"--tw-backdrop-saturate":`saturate(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropSaturate")})},backdropSepia:({matchUtilities:r,theme:e})=>{r({"backdrop-sepia":t=>({"--tw-backdrop-sepia":`sepia(${t})`,"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge})},{values:e("backdropSepia")})},backdropFilter:({addDefaults:r,addUtilities:e})=>{r("backdrop-filter",{"--tw-backdrop-blur":" ","--tw-backdrop-brightness":" ","--tw-backdrop-contrast":" ","--tw-backdrop-grayscale":" ","--tw-backdrop-hue-rotate":" ","--tw-backdrop-invert":" ","--tw-backdrop-opacity":" ","--tw-backdrop-saturate":" ","--tw-backdrop-sepia":" "}),e({".backdrop-filter":{"@defaults backdrop-filter":{},"-webkit-backdrop-filter":ge,"backdrop-filter":ge},".backdrop-filter-none":{"-webkit-backdrop-filter":"none","backdrop-filter":"none"}})},transitionProperty:({matchUtilities:r,theme:e})=>{let t=e("transitionTimingFunction.DEFAULT"),i=e("transitionDuration.DEFAULT");r({transition:n=>({"transition-property":n,...n==="none"?{}:{"transition-timing-function":t,"transition-duration":i}})},{values:e("transitionProperty")})},transitionDelay:L("transitionDelay",[["delay",["transitionDelay"]]]),transitionDuration:L("transitionDuration",[["duration",["transitionDuration"]]],{filterDefault:!0}),transitionTimingFunction:L("transitionTimingFunction",[["ease",["transitionTimingFunction"]]],{filterDefault:!0}),willChange:L("willChange",[["will-change",["will-change"]]]),contain:({addDefaults:r,addUtilities:e})=>{let t="var(--tw-contain-size) var(--tw-contain-layout) var(--tw-contain-paint) var(--tw-contain-style)";r("contain",{"--tw-contain-size":" ","--tw-contain-layout":" ","--tw-contain-paint":" ","--tw-contain-style":" "}),e({".contain-none":{contain:"none"},".contain-content":{contain:"content"},".contain-strict":{contain:"strict"},".contain-size":{"@defaults contain":{},"--tw-contain-size":"size",contain:t},".contain-inline-size":{"@defaults contain":{},"--tw-contain-size":"inline-size",contain:t},".contain-layout":{"@defaults contain":{},"--tw-contain-layout":"layout",contain:t},".contain-paint":{"@defaults contain":{},"--tw-contain-paint":"paint",contain:t},".contain-style":{"@defaults contain":{},"--tw-contain-style":"style",contain:t}})},content:L("content",[["content",["--tw-content",["content","var(--tw-content)"]]]]),forcedColorAdjust:({addUtilities:r})=>{r({".forced-color-adjust-auto":{"forced-color-adjust":"auto"},".forced-color-adjust-none":{"forced-color-adjust":"none"}})}}});function h_(r){if(r===void 0)return!1;if(r==="true"||r==="1")return!0;if(r==="false"||r==="0")return!1;if(r==="*")return!0;let e=r.split(",").map(t=>t.split(":")[0]);return e.includes("-tailwindcss")?!1:!!e.includes("tailwindcss")}var Ze,wh,vh,es,No,gt,Ti,It=P(()=>{u();Ze=typeof m!="undefined"?{NODE_ENV:"production",DEBUG:h_(m.env.DEBUG)}:{NODE_ENV:"production",DEBUG:!1},wh=new Map,vh=new Map,es=new Map,No=new Map,gt=new String("*"),Ti=Symbol("__NONE__")});function cr(r){let e=[],t=!1;for(let i=0;i0)}var xh,kh,m_,Bo=P(()=>{u();xh=new Map([["{","}"],["[","]"],["(",")"]]),kh=new Map(Array.from(xh.entries()).map(([r,e])=>[e,r])),m_=new Set(['"',"'","`"])});function pr(r){let[e]=Sh(r);return e.forEach(([t,i])=>t.removeChild(i)),r.nodes.push(...e.map(([,t])=>t)),r}function Sh(r){let e=[],t=null;for(let i of r.nodes)if(i.type==="combinator")e=e.filter(([,n])=>jo(n).includes("jumpable")),t=null;else if(i.type==="pseudo"){g_(i)?(t=i,e.push([r,i,null])):t&&y_(i,t)?e.push([r,i,t]):t=null;for(let n of i.nodes??[]){let[s,a]=Sh(n);t=a||t,e.push(...s)}}return[e,t]}function Ah(r){return r.value.startsWith("::")||Fo[r.value]!==void 0}function g_(r){return Ah(r)&&jo(r).includes("terminal")}function y_(r,e){return r.type!=="pseudo"||Ah(r)?!1:jo(e).includes("actionable")}function jo(r){return Fo[r.value]??Fo.__default__}var Fo,ts=P(()=>{u();Fo={"::after":["terminal","jumpable"],"::backdrop":["terminal","jumpable"],"::before":["terminal","jumpable"],"::cue":["terminal"],"::cue-region":["terminal"],"::first-letter":["terminal","jumpable"],"::first-line":["terminal","jumpable"],"::grammar-error":["terminal"],"::marker":["terminal","jumpable"],"::part":["terminal","actionable"],"::placeholder":["terminal","jumpable"],"::selection":["terminal","jumpable"],"::slotted":["terminal"],"::spelling-error":["terminal"],"::target-text":["terminal"],"::file-selector-button":["terminal","actionable"],"::deep":["actionable"],"::v-deep":["actionable"],"::ng-deep":["actionable"],":after":["terminal","jumpable"],":before":["terminal","jumpable"],":first-letter":["terminal","jumpable"],":first-line":["terminal","jumpable"],":where":[],":is":[],":has":[],__default__:["terminal","actionable"]}});function dr(r,{context:e,candidate:t}){let i=e?.tailwindConfig.prefix??"",n=r.map(a=>{let o=(0,st.default)().astSync(a.format);return{...a,ast:a.respectPrefix?ur(i,o):o}}),s=st.default.root({nodes:[st.default.selector({nodes:[st.default.className({value:Te(t)})]})]});for(let{ast:a}of n)[s,a]=w_(s,a),a.walkNesting(o=>o.replaceWith(...s.nodes[0].nodes)),s=a;return s}function _h(r){let e=[];for(;r.prev()&&r.prev().type!=="combinator";)r=r.prev();for(;r&&r.type!=="combinator";)e.push(r),r=r.next();return e}function b_(r){return r.sort((e,t)=>e.type==="tag"&&t.type==="class"?-1:e.type==="class"&&t.type==="tag"?1:e.type==="class"&&t.type==="pseudo"&&t.value.startsWith("::")?-1:e.type==="pseudo"&&e.value.startsWith("::")&&t.type==="class"?1:r.index(e)-r.index(t)),r}function Uo(r,e){let t=!1;r.walk(i=>{if(i.type==="class"&&i.value===e)return t=!0,!1}),t||r.remove()}function rs(r,e,{context:t,candidate:i,base:n}){let s=t?.tailwindConfig?.separator??":";n=n??ve(i,s).pop();let a=(0,st.default)().astSync(r);if(a.walkClasses(f=>{f.raws&&f.value.includes(n)&&(f.raws.value=Te((0,Ch.default)(f.raws.value)))}),a.each(f=>Uo(f,n)),a.length===0)return null;let o=Array.isArray(e)?dr(e,{context:t,candidate:i}):e;if(o===null)return a.toString();let l=st.default.comment({value:"/*__simple__*/"}),c=st.default.comment({value:"/*__simple__*/"});return a.walkClasses(f=>{if(f.value!==n)return;let d=f.parent,p=o.nodes[0].nodes;if(d.nodes.length===1){f.replaceWith(...p);return}let h=_h(f);d.insertBefore(h[0],l),d.insertAfter(h[h.length-1],c);for(let v of p)d.insertBefore(h[0],v.clone());f.remove(),h=_h(l);let b=d.index(l);d.nodes.splice(b,h.length,...b_(st.default.selector({nodes:h})).nodes),l.remove(),c.remove()}),a.walkPseudos(f=>{f.value===zo&&f.replaceWith(f.nodes)}),a.each(f=>pr(f)),a.toString()}function w_(r,e){let t=[];return r.walkPseudos(i=>{i.value===zo&&t.push({pseudo:i,value:i.nodes[0].toString()})}),e.walkPseudos(i=>{if(i.value!==zo)return;let n=i.nodes[0].toString(),s=t.find(c=>c.value===n);if(!s)return;let a=[],o=i.next();for(;o&&o.type!=="combinator";)a.push(o),o=o.next();let l=o;s.pseudo.parent.insertAfter(s.pseudo,st.default.selector({nodes:a.map(c=>c.clone())})),i.remove(),a.forEach(c=>c.remove()),l&&l.type==="combinator"&&l.remove()}),[r,e]}var st,Ch,zo,Vo=P(()=>{u();st=pe(it()),Ch=pe(Pn());fr();Gn();ts();zt();zo=":merge"});function is(r,e){let t=(0,Ho.default)().astSync(r);return t.each(i=>{i.nodes.some(s=>s.type==="combinator")&&(i.nodes=[Ho.default.pseudo({value:":is",nodes:[i.clone()]})]),pr(i)}),`${e} ${t.toString()}`}var Ho,Wo=P(()=>{u();Ho=pe(it());ts()});function Go(r){return v_.transformSync(r)}function*x_(r){let e=1/0;for(;e>=0;){let t,i=!1;if(e===1/0&&r.endsWith("]")){let a=r.indexOf("[");r[a-1]==="-"?t=a-1:r[a-1]==="/"?(t=a-1,i=!0):t=-1}else e===1/0&&r.includes("/")?(t=r.lastIndexOf("/"),i=!0):t=r.lastIndexOf("-",e);if(t<0)break;let n=r.slice(0,t),s=r.slice(i?t:t+1);e=t-1,!(n===""||s==="/")&&(yield[n,s])}}function k_(r,e){if(r.length===0||e.tailwindConfig.prefix==="")return r;for(let t of r){let[i]=t;if(i.options.respectPrefix){let n=ee.root({nodes:[t[1].clone()]}),s=t[1].raws.tailwind.classCandidate;n.walkRules(a=>{let o=s.startsWith("-");a.selector=ur(e.tailwindConfig.prefix,a.selector,o)}),t[1]=n.nodes[0]}}return r}function S_(r,e){if(r.length===0)return r;let t=[];function i(n){return n.parent&&n.parent.type==="atrule"&&n.parent.name==="keyframes"}for(let[n,s]of r){let a=ee.root({nodes:[s.clone()]});a.walkRules(o=>{if(i(o))return;let l=(0,ns.default)().astSync(o.selector);l.each(c=>Uo(c,e)),Qf(l,c=>c===e?`!${c}`:c),o.selector=l.toString(),o.walkDecls(c=>c.important=!0)}),t.push([{...n,important:!0},a.nodes[0]])}return t}function A_(r,e,t){if(e.length===0)return e;let i={modifier:null,value:Ti};{let[n,...s]=ve(r,"/");if(s.length>1&&(n=n+"/"+s.slice(0,-1).join("/"),s=s.slice(-1)),s.length&&!t.variantMap.has(r)&&(r=n,i.modifier=s[0],!we(t.tailwindConfig,"generalizedModifiers")))return[]}if(r.endsWith("]")&&!r.startsWith("[")){let n=/(.)(-?)\[(.*)\]/g.exec(r);if(n){let[,s,a,o]=n;if(s==="@"&&a==="-")return[];if(s!=="@"&&a==="")return[];r=r.replace(`${a}[${o}]`,""),i.value=o}}if(Ko(r)&&!t.variantMap.has(r)){let n=t.offsets.recordVariant(r),s=K(r.slice(1,-1)),a=ve(s,",");if(a.length>1)return[];if(!a.every(ls))return[];let o=a.map((l,c)=>[t.offsets.applyParallelOffset(n,c),Ri(l.trim())]);t.variantMap.set(r,o)}if(t.variantMap.has(r)){let n=Ko(r),s=t.variantOptions.get(r)?.[Pt]??{},a=t.variantMap.get(r).slice(),o=[],l=(()=>!(n||s.respectPrefix===!1))();for(let[c,f]of e){if(c.layer==="user")continue;let d=ee.root({nodes:[f.clone()]});for(let[p,h,b]of a){let w=function(){v.raws.neededBackup||(v.raws.neededBackup=!0,v.walkRules(T=>T.raws.originalSelector=T.selector))},k=function(T){return w(),v.each(B=>{B.type==="rule"&&(B.selectors=B.selectors.map(N=>T({get className(){return Go(N)},selector:N})))}),v},v=(b??d).clone(),y=[],S=h({get container(){return w(),v},separator:t.tailwindConfig.separator,modifySelectors:k,wrap(T){let B=v.nodes;v.removeAll(),T.append(B),v.append(T)},format(T){y.push({format:T,respectPrefix:l})},args:i});if(Array.isArray(S)){for(let[T,B]of S.entries())a.push([t.offsets.applyParallelOffset(p,T),B,v.clone()]);continue}if(typeof S=="string"&&y.push({format:S,respectPrefix:l}),S===null)continue;v.raws.neededBackup&&(delete v.raws.neededBackup,v.walkRules(T=>{let B=T.raws.originalSelector;if(!B||(delete T.raws.originalSelector,B===T.selector))return;let N=T.selector,R=(0,ns.default)(F=>{F.walkClasses(Y=>{Y.value=`${r}${t.tailwindConfig.separator}${Y.value}`})}).processSync(B);y.push({format:N.replace(R,"&"),respectPrefix:l}),T.selector=B})),v.nodes[0].raws.tailwind={...v.nodes[0].raws.tailwind,parentLayer:c.layer};let E=[{...c,sort:t.offsets.applyVariantOffset(c.sort,p,Object.assign(i,t.variantOptions.get(r))),collectedFormats:(c.collectedFormats??[]).concat(y)},v.nodes[0]];o.push(E)}}return o}return[]}function Qo(r,e,t={}){return!ke(r)&&!Array.isArray(r)?[[r],t]:Array.isArray(r)?Qo(r[0],e,r[1]):(e.has(r)||e.set(r,lr(r)),[e.get(r),t])}function __(r){return C_.test(r)}function E_(r){if(!r.includes("://"))return!1;try{let e=new URL(r);return e.scheme!==""&&e.host!==""}catch(e){return!1}}function Eh(r){let e=!0;return r.walkDecls(t=>{if(!Oh(t.prop,t.value))return e=!1,!1}),e}function Oh(r,e){if(E_(`${r}:${e}`))return!1;try{return ee.parse(`a{${r}:${e}}`).toResult(),!0}catch(t){return!1}}function O_(r,e){let[,t,i]=r.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/)??[];if(i===void 0||!__(t)||!cr(i))return null;let n=K(i,{property:t});return Oh(t,n)?[[{sort:e.offsets.arbitraryProperty(r),layer:"utilities",options:{respectImportant:!0}},()=>({[$o(r)]:{[t]:n}})]]:null}function*T_(r,e){e.candidateRuleMap.has(r)&&(yield[e.candidateRuleMap.get(r),"DEFAULT"]),yield*function*(o){o!==null&&(yield[o,"DEFAULT"])}(O_(r,e));let t=r,i=!1,n=e.tailwindConfig.prefix,s=n.length,a=t.startsWith(n)||t.startsWith(`-${n}`);t[s]==="-"&&a&&(i=!0,t=n+t.slice(s+1)),i&&e.candidateRuleMap.has(t)&&(yield[e.candidateRuleMap.get(t),"-DEFAULT"]);for(let[o,l]of x_(t))e.candidateRuleMap.has(o)&&(yield[e.candidateRuleMap.get(o),i?`-${l}`:l])}function R_(r,e){return r===gt?[gt]:ve(r,e)}function*P_(r,e){for(let t of r)t[1].raws.tailwind={...t[1].raws.tailwind,classCandidate:e,preserveSource:t[0].options?.preserveSource??!1},yield t}function*Yo(r,e){let t=e.tailwindConfig.separator,[i,...n]=R_(r,t).reverse(),s=!1;i.startsWith("!")&&(s=!0,i=i.slice(1));for(let a of T_(i,e)){let o=[],l=new Map,[c,f]=a,d=c.length===1;for(let[p,h]of c){let b=[];if(typeof h=="function")for(let v of[].concat(h(f,{isOnlyPlugin:d}))){let[y,w]=Qo(v,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}else if(f==="DEFAULT"||f==="-DEFAULT"){let v=h,[y,w]=Qo(v,e.postCssNodeCache);for(let k of y)b.push([{...p,options:{...p.options,...w}},k])}if(b.length>0){let v=Array.from(ta(p.options?.types??[],f,p.options??{},e.tailwindConfig)).map(([y,w])=>w);v.length>0&&l.set(b,v),o.push(b)}}if(Ko(f)){if(o.length>1){let b=function(y){return y.length===1?y[0]:y.find(w=>{let k=l.get(w);return w.some(([{options:S},E])=>Eh(E)?S.types.some(({type:T,preferOnConflict:B})=>k.includes(T)&&B):!1)})},[p,h]=o.reduce((y,w)=>(w.some(([{options:S}])=>S.types.some(({type:E})=>E==="any"))?y[0].push(w):y[1].push(w),y),[[],[]]),v=b(h)??b(p);if(v)o=[v];else{let y=o.map(k=>new Set([...l.get(k)??[]]));for(let k of y)for(let S of k){let E=!1;for(let T of y)k!==T&&T.has(S)&&(T.delete(S),E=!0);E&&k.delete(S)}let w=[];for(let[k,S]of y.entries())for(let E of S){let T=o[k].map(([,B])=>B).flat().map(B=>B.toString().split(` -`).slice(1,-1).map(N=>N.trim()).map(N=>` ${N}`).join(` -`)).join(` - -`);w.push(` Use \`${r.replace("[",`[${E}:`)}\` for \`${T.trim()}\``);break}G.warn([`The class \`${r}\` is ambiguous and matches multiple utilities.`,...w,`If this is content and not a class, replace it with \`${r.replace("[","[").replace("]","]")}\` to silence this warning.`]);continue}}o=o.map(p=>p.filter(h=>Eh(h[1])))}o=o.flat(),o=Array.from(P_(o,i)),o=k_(o,e),s&&(o=S_(o,i));for(let p of n)o=A_(p,o,e);for(let p of o)p[1].raws.tailwind={...p[1].raws.tailwind,candidate:r},p=I_(p,{context:e,candidate:r}),p!==null&&(yield p)}}function I_(r,{context:e,candidate:t}){if(!r[0].collectedFormats)return r;let i=!0,n;try{n=dr(r[0].collectedFormats,{context:e,candidate:t})}catch{return null}let s=ee.root({nodes:[r[1].clone()]});return s.walkRules(a=>{if(!ss(a))try{let o=rs(a.selector,n,{candidate:t,context:e});if(o===null){a.remove();return}a.selector=o}catch{return i=!1,!1}}),!i||s.nodes.length===0?null:(r[1]=s.nodes[0],r)}function ss(r){return r.parent&&r.parent.type==="atrule"&&r.parent.name==="keyframes"}function D_(r){if(r===!0)return e=>{ss(e)||e.walkDecls(t=>{t.parent.type==="rule"&&!ss(t.parent)&&(t.important=!0)})};if(typeof r=="string")return e=>{ss(e)||(e.selectors=e.selectors.map(t=>is(t,r)))}}function as(r,e,t=!1){let i=[],n=D_(e.tailwindConfig.important);for(let s of r){if(e.notClassCache.has(s))continue;if(e.candidateRuleCache.has(s)){i=i.concat(Array.from(e.candidateRuleCache.get(s)));continue}let a=Array.from(Yo(s,e));if(a.length===0){e.notClassCache.add(s);continue}e.classCache.set(s,a);let o=e.candidateRuleCache.get(s)??new Set;e.candidateRuleCache.set(s,o);for(let l of a){let[{sort:c,options:f},d]=l;if(f.respectImportant&&n){let h=ee.root({nodes:[d.clone()]});h.walkRules(n),d=h.nodes[0]}let p=[c,t?d.clone():d];o.add(p),e.ruleCache.add(p),i.push(p)}}return i}function Ko(r){return r.startsWith("[")&&r.endsWith("]")}var ns,v_,C_,os=P(()=>{u();Ot();ns=pe(it());qo();Kt();Gn();Fr();Be();It();Vo();Lo();Br();Oi();Bo();zt();ct();Wo();v_=(0,ns.default)(r=>r.first.filter(({type:e})=>e==="class").pop().value);C_=/^[a-z_-]/});var Th,Rh=P(()=>{u();Th={}});function q_(r){try{return Th.createHash("md5").update(r,"utf-8").digest("binary")}catch(e){return""}}function Ph(r,e){let t=e.toString();if(!t.includes("@tailwind"))return!1;let i=No.get(r),n=q_(t),s=i!==n;return No.set(r,n),s}var Ih=P(()=>{u();Rh();It()});function us(r){return(r>0n)-(r<0n)}var Dh=P(()=>{u()});function qh(r,e){let t=0n,i=0n;for(let[n,s]of e)r&n&&(t=t|n,i=i|s);return r&~t|i}var $h=P(()=>{u()});function Lh(r){let e=null;for(let t of r)e=e??t,e=e>t?e:t;return e}function $_(r,e){let t=r.length,i=e.length,n=t{u();Dh();$h();Xo=class{constructor(){this.offsets={defaults:0n,base:0n,components:0n,utilities:0n,variants:0n,user:0n},this.layerPositions={defaults:0n,base:1n,components:2n,utilities:3n,user:4n,variants:5n},this.reservedVariantBits=0n,this.variantOffsets=new Map}create(e){return{layer:e,parentLayer:e,arbitrary:0n,variants:0n,parallelIndex:0n,index:this.offsets[e]++,propertyOffset:0n,property:"",options:[]}}arbitraryProperty(e){return{...this.create("utilities"),arbitrary:1n,property:e}}forVariant(e,t=0){let i=this.variantOffsets.get(e);if(i===void 0)throw new Error(`Cannot find offset for unknown variant ${e}`);return{...this.create("variants"),variants:i<n.startsWith("[")).sort(([n],[s])=>$_(n,s)),t=e.map(([,n])=>n).sort((n,s)=>us(n-s));return e.map(([,n],s)=>[n,t[s]]).filter(([n,s])=>n!==s)}remapArbitraryVariantOffsets(e){let t=this.recalculateVariantOffsets();return t.length===0?e:e.map(i=>{let[n,s]=i;return n={...n,variants:qh(n.variants,t)},[n,s]})}sortArbitraryProperties(e){let t=new Set;for(let[a]of e)a.arbitrary===1n&&t.add(a.property);if(t.size===0)return e;let i=Array.from(t).sort(),n=new Map,s=1n;for(let a of i)n.set(a,s++);return e.map(a=>{let[o,l]=a;return o={...o,propertyOffset:n.get(o.property)??0n},[o,l]})}sort(e){return e=this.remapArbitraryVariantOffsets(e),e=this.sortArbitraryProperties(e),e.sort(([t],[i])=>us(this.compare(t,i)))}}});function tl(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function Bh({type:r="any",...e}){let t=[].concat(r);return{...e,types:t.map(i=>Array.isArray(i)?{type:i[0],...i[1]}:{type:i,preferOnConflict:!1})}}function L_(r){let e=[],t="",i=0;for(let n=0;n0&&e.push(t.trim()),e=e.filter(n=>n!==""),e}function M_(r,e,{before:t=[]}={}){if(t=[].concat(t),t.length<=0){r.push(e);return}let i=r.length-1;for(let n of t){let s=r.indexOf(n);s!==-1&&(i=Math.min(i,s))}r.splice(i,0,e)}function Fh(r){return Array.isArray(r)?r.flatMap(e=>!Array.isArray(e)&&!ke(e)?e:lr(e)):Fh([r])}function N_(r,e){return(0,Zo.default)(i=>{let n=[];return e&&e(i),i.walkClasses(s=>{n.push(s.value)}),n}).transformSync(r)}function B_(r){r.walkPseudos(e=>{e.value===":not"&&e.remove()})}function F_(r,e={containsNonOnDemandable:!1},t=0){let i=[],n=[];r.type==="rule"?n.push(...r.selectors):r.type==="atrule"&&r.walkRules(s=>n.push(...s.selectors));for(let s of n){let a=N_(s,B_);a.length===0&&(e.containsNonOnDemandable=!0);for(let o of a)i.push(o)}return t===0?[e.containsNonOnDemandable||i.length===0,i]:i}function fs(r){return Fh(r).flatMap(e=>{let t=new Map,[i,n]=F_(e);return i&&n.unshift(gt),n.map(s=>(t.has(e)||t.set(e,e),[s,t.get(e)]))})}function ls(r){return r.startsWith("@")||r.includes("&")}function Ri(r){r=r.replace(/\n+/g,"").replace(/\s{1,}/g," ").trim();let e=L_(r).map(t=>{if(!t.startsWith("@"))return({format:s})=>s(t);let[,i,n]=/@(\S*)( .+|[({].*)?/g.exec(t);return({wrap:s})=>s(ee.atRule({name:i,params:n?.trim()??""}))}).reverse();return t=>{for(let i of e)i(t)}}function j_(r,e,{variantList:t,variantMap:i,offsets:n,classList:s}){function a(p,h){return p?(0,Nh.default)(r,p,h):r}function o(p){return ur(r.prefix,p)}function l(p,h){return p===gt?gt:h.respectPrefix?e.tailwindConfig.prefix+p:p}function c(p,h,b={}){let v=kt(p),y=a(["theme",...v],h);return mt(v[0])(y,b)}let f=0,d={postcss:ee,prefix:o,e:Te,config:a,theme:c,corePlugins:p=>Array.isArray(r.corePlugins)?r.corePlugins.includes(p):a(["corePlugins",p],!0),variants:()=>[],addBase(p){for(let[h,b]of fs(p)){let v=l(h,{}),y=n.create("base");e.candidateRuleMap.has(v)||e.candidateRuleMap.set(v,[]),e.candidateRuleMap.get(v).push([{sort:y,layer:"base"},b])}},addDefaults(p,h){let b={[`@defaults ${p}`]:h};for(let[v,y]of fs(b)){let w=l(v,{});e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("defaults"),layer:"defaults"},y])}},addComponents(p,h){h=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!1},Array.isArray(h)?{}:h);for(let[v,y]of fs(p)){let w=l(v,h);s.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("components"),layer:"components",options:h},y])}},addUtilities(p,h){h=Object.assign({},{preserveSource:!1,respectPrefix:!0,respectImportant:!0},Array.isArray(h)?{}:h);for(let[v,y]of fs(p)){let w=l(v,h);s.add(w),e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push([{sort:n.create("utilities"),layer:"utilities",options:h},y])}},matchUtilities:function(p,h){h=Bh({...{respectPrefix:!0,respectImportant:!0,modifiers:!1},...h});let v=n.create("utilities");for(let y in p){let S=function(T,{isOnlyPlugin:B}){let[N,R,F]=ea(h.types,T,h,r);if(N===void 0)return[];if(!h.types.some(({type:U})=>U===R))if(B)G.warn([`Unnecessary typehint \`${R}\` in \`${y}-${T}\`.`,`You can safely update it to \`${y}-${T.replace(R+":","")}\`.`]);else return[];if(!cr(N))return[];let Y={get modifier(){return h.modifiers||G.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),F}},_=we(r,"generalizedModifiers");return[].concat(_?k(N,Y):k(N)).filter(Boolean).map(U=>({[Qn(y,T)]:U}))},w=l(y,h),k=p[y];s.add([w,h]);let E=[{sort:v,layer:"utilities",options:h},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(E)}},matchComponents:function(p,h){h=Bh({...{respectPrefix:!0,respectImportant:!1,modifiers:!1},...h});let v=n.create("components");for(let y in p){let S=function(T,{isOnlyPlugin:B}){let[N,R,F]=ea(h.types,T,h,r);if(N===void 0)return[];if(!h.types.some(({type:U})=>U===R))if(B)G.warn([`Unnecessary typehint \`${R}\` in \`${y}-${T}\`.`,`You can safely update it to \`${y}-${T.replace(R+":","")}\`.`]);else return[];if(!cr(N))return[];let Y={get modifier(){return h.modifiers||G.warn(`modifier-used-without-options-for-${y}`,["Your plugin must set `modifiers: true` in its options to support modifiers."]),F}},_=we(r,"generalizedModifiers");return[].concat(_?k(N,Y):k(N)).filter(Boolean).map(U=>({[Qn(y,T)]:U}))},w=l(y,h),k=p[y];s.add([w,h]);let E=[{sort:v,layer:"components",options:h},S];e.candidateRuleMap.has(w)||e.candidateRuleMap.set(w,[]),e.candidateRuleMap.get(w).push(E)}},addVariant(p,h,b={}){h=[].concat(h).map(v=>{if(typeof v!="string")return(y={})=>{let{args:w,modifySelectors:k,container:S,separator:E,wrap:T,format:B}=y,N=v(Object.assign({modifySelectors:k,container:S,separator:E},b.type===Jo.MatchVariant&&{args:w,wrap:T,format:B}));if(typeof N=="string"&&!ls(N))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Array.isArray(N)?N.filter(R=>typeof R=="string").map(R=>Ri(R)):N&&typeof N=="string"&&Ri(N)(y)};if(!ls(v))throw new Error(`Your custom variant \`${p}\` has an invalid format string. Make sure it's an at-rule or contains a \`&\` placeholder.`);return Ri(v)}),M_(t,p,b),i.set(p,h),e.variantOptions.set(p,b)},matchVariant(p,h,b){let v=b?.id??++f,y=p==="@",w=we(r,"generalizedModifiers");for(let[S,E]of Object.entries(b?.values??{}))S!=="DEFAULT"&&d.addVariant(y?`${p}${S}`:`${p}-${S}`,({args:T,container:B})=>h(E,w?{modifier:T?.modifier,container:B}:{container:B}),{...b,value:E,id:v,type:Jo.MatchVariant,variantInfo:el.Base});let k="DEFAULT"in(b?.values??{});d.addVariant(p,({args:S,container:E})=>S?.value===Ti&&!k?null:h(S?.value===Ti?b.values.DEFAULT:S?.value??(typeof S=="string"?S:""),w?{modifier:S?.modifier,container:E}:{container:E}),{...b,id:v,type:Jo.MatchVariant,variantInfo:el.Dynamic})}};return d}function cs(r){return rl.has(r)||rl.set(r,new Map),rl.get(r)}function jh(r,e){let t=!1,i=new Map;for(let n of r){if(!n)continue;let s=oa.parse(n),a=s.hash?s.href.replace(s.hash,""):s.href;a=s.search?a.replace(s.search,""):a;let o=be.statSync(decodeURIComponent(a),{throwIfNoEntry:!1})?.mtimeMs;!o||((!e.has(n)||o>e.get(n))&&(t=!0),i.set(n,o))}return[t,i]}function zh(r){r.walkAtRules(e=>{["responsive","variants"].includes(e.name)&&(zh(e),e.before(e.nodes),e.remove())})}function z_(r){let e=[];return r.each(t=>{t.type==="atrule"&&["responsive","variants"].includes(t.name)&&(t.name="layer",t.params="utilities")}),r.walkAtRules("layer",t=>{if(zh(t),t.params==="base"){for(let i of t.nodes)e.push(function({addBase:n}){n(i,{respectPrefix:!1})});t.remove()}else if(t.params==="components"){for(let i of t.nodes)e.push(function({addComponents:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}else if(t.params==="utilities"){for(let i of t.nodes)e.push(function({addUtilities:n}){n(i,{respectPrefix:!1,preserveSource:!0})});t.remove()}}),e}function U_(r,e){let t=Object.entries({...se,...yh}).map(([l,c])=>r.tailwindConfig.corePlugins.includes(l)?c:null).filter(Boolean),i=r.tailwindConfig.plugins.map(l=>(l.__isOptionsFunction&&(l=l()),typeof l=="function"?l:l.handler)),n=z_(e),s=[se.childVariant,se.pseudoElementVariants,se.pseudoClassVariants,se.hasVariants,se.ariaVariants,se.dataVariants],a=[se.supportsVariants,se.reducedMotionVariants,se.prefersContrastVariants,se.screenVariants,se.orientationVariants,se.directionVariants,se.darkVariants,se.forcedColorsVariants,se.printVariant];return(r.tailwindConfig.darkMode==="class"||Array.isArray(r.tailwindConfig.darkMode)&&r.tailwindConfig.darkMode[0]==="class")&&(a=[se.supportsVariants,se.reducedMotionVariants,se.prefersContrastVariants,se.darkVariants,se.screenVariants,se.orientationVariants,se.directionVariants,se.forcedColorsVariants,se.printVariant]),[...t,...s,...i,...a,...n]}function V_(r,e){let t=[],i=new Map;e.variantMap=i;let n=new Xo;e.offsets=n;let s=new Set,a=j_(e.tailwindConfig,e,{variantList:t,variantMap:i,offsets:n,classList:s});for(let f of r)if(Array.isArray(f))for(let d of f)d(a);else f?.(a);n.recordVariants(t,f=>i.get(f).length);for(let[f,d]of i.entries())e.variantMap.set(f,d.map((p,h)=>[n.forVariant(f,h),p]));let o=(e.tailwindConfig.safelist??[]).filter(Boolean);if(o.length>0){let f=[];for(let d of o){if(typeof d=="string"){e.changedContent.push({content:d,extension:"html"});continue}if(d instanceof RegExp){G.warn("root-regex",["Regular expressions in `safelist` work differently in Tailwind CSS v3.0.","Update your `safelist` configuration to eliminate this warning.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"]);continue}f.push(d)}if(f.length>0){let d=new Map,p=e.tailwindConfig.prefix.length,h=f.some(b=>b.pattern.source.includes("!"));for(let b of s){let v=Array.isArray(b)?(()=>{let[y,w]=b,S=Object.keys(w?.values??{}).map(E=>Ei(y,E));return w?.supportsNegativeValues&&(S=[...S,...S.map(E=>"-"+E)],S=[...S,...S.map(E=>E.slice(0,p)+"-"+E.slice(p))]),w.types.some(({type:E})=>E==="color")&&(S=[...S,...S.flatMap(E=>Object.keys(e.tailwindConfig.theme.opacity).map(T=>`${E}/${T}`))]),h&&w?.respectImportant&&(S=[...S,...S.map(E=>"!"+E)]),S})():[b];for(let y of v)for(let{pattern:w,variants:k=[]}of f)if(w.lastIndex=0,d.has(w)||d.set(w,0),!!w.test(y)){d.set(w,d.get(w)+1),e.changedContent.push({content:y,extension:"html"});for(let S of k)e.changedContent.push({content:S+e.tailwindConfig.separator+y,extension:"html"})}}for(let[b,v]of d.entries())v===0&&G.warn([`The safelist pattern \`${b}\` doesn't match any Tailwind CSS classes.`,"Fix this pattern or remove it from your `safelist` configuration.","https://tailwindcss.com/docs/content-configuration#safelisting-classes"])}}let l=[].concat(e.tailwindConfig.darkMode??"media")[1]??"dark",c=[tl(e,l),tl(e,"group"),tl(e,"peer")];e.getClassOrder=function(d){let p=[...d].sort((y,w)=>y===w?0:y[y,null])),b=as(new Set(p),e,!0);b=e.offsets.sort(b);let v=BigInt(c.length);for(let[,y]of b){let w=y.raws.tailwind.candidate;h.set(w,h.get(w)??v++)}return d.map(y=>{let w=h.get(y)??null,k=c.indexOf(y);return w===null&&k!==-1&&(w=BigInt(k)),[y,w]})},e.getClassList=function(d={}){let p=[];for(let h of s)if(Array.isArray(h)){let[b,v]=h,y=[],w=Object.keys(v?.modifiers??{});v?.types?.some(({type:E})=>E==="color")&&w.push(...Object.keys(e.tailwindConfig.theme.opacity??{}));let k={modifiers:w},S=d.includeMetadata&&w.length>0;for(let[E,T]of Object.entries(v?.values??{})){if(T==null)continue;let B=Ei(b,E);if(p.push(S?[B,k]:B),v?.supportsNegativeValues&&xt(T)){let N=Ei(b,`-${E}`);y.push(S?[N,k]:N)}}p.push(...y)}else p.push(h);return p},e.getVariants=function(){let d=Math.random().toString(36).substring(7).toUpperCase(),p=[];for(let[h,b]of e.variantOptions.entries())b.variantInfo!==el.Base&&p.push({name:h,isArbitrary:b.type===Symbol.for("MATCH_VARIANT"),values:Object.keys(b.values??{}),hasDash:h!=="@",selectors({modifier:v,value:y}={}){let w=`TAILWINDPLACEHOLDER${d}`,k=ee.rule({selector:`.${w}`}),S=ee.root({nodes:[k.clone()]}),E=S.toString(),T=(e.variantMap.get(h)??[]).flatMap(([le,A])=>A),B=[];for(let le of T){let A=[],C={args:{modifier:v,value:b.values?.[y]??y},separator:e.tailwindConfig.separator,modifySelectors(V){return S.each(Ee=>{Ee.type==="rule"&&(Ee.selectors=Ee.selectors.map(Ie=>V({get className(){return Go(Ie)},selector:Ie})))}),S},format(V){A.push(V)},wrap(V){A.push(`@${V.name} ${V.params} { & }`)},container:S},he=le(C);if(A.length>0&&B.push(A),Array.isArray(he))for(let V of he)A=[],V(C),B.push(A)}let N=[],R=S.toString();E!==R&&(S.walkRules(le=>{let A=le.selector,C=(0,Zo.default)(he=>{he.walkClasses(V=>{V.value=`${h}${e.tailwindConfig.separator}${V.value}`})}).processSync(A);N.push(A.replace(C,"&").replace(w,"&"))}),S.walkAtRules(le=>{N.push(`@${le.name} (${le.params}) { & }`)}));let F=!(y in(b.values??{})),Y=b[Pt]??{},_=(()=>!(F||Y.respectPrefix===!1))();B=B.map(le=>le.map(A=>({format:A,respectPrefix:_}))),N=N.map(le=>({format:le,respectPrefix:_}));let Q={candidate:w,context:e},U=B.map(le=>rs(`.${w}`,dr(le,Q),Q).replace(`.${w}`,"&").replace("{ & }","").trim());return N.length>0&&U.push(dr(N,Q).toString().replace(`.${w}`,"&")),U}});return p}}function Uh(r,e){!r.classCache.has(e)||(r.notClassCache.add(e),r.classCache.delete(e),r.applyClassCache.delete(e),r.candidateRuleMap.delete(e),r.candidateRuleCache.delete(e),r.stylesheetCache=null)}function H_(r,e){let t=e.raws.tailwind.candidate;if(!!t){for(let i of r.ruleCache)i[1].raws.tailwind.candidate===t&&r.ruleCache.delete(i);Uh(r,t)}}function il(r,e=[],t=ee.root()){let i={disposables:[],ruleCache:new Set,candidateRuleCache:new Map,classCache:new Map,applyClassCache:new Map,notClassCache:new Set(r.blocklist??[]),postCssNodeCache:new Map,candidateRuleMap:new Map,tailwindConfig:r,changedContent:e,variantMap:new Map,stylesheetCache:null,variantOptions:new Map,markInvalidUtilityCandidate:s=>Uh(i,s),markInvalidUtilityNode:s=>H_(i,s)},n=U_(i,t);return V_(n,i),i}function Vh(r,e,t,i,n,s){let a=e.opts.from,o=i!==null;Ze.DEBUG&&console.log("Source path:",a);let l;if(o&&hr.has(a))l=hr.get(a);else if(Pi.has(n)){let p=Pi.get(n);Dt.get(p).add(a),hr.set(a,p),l=p}let c=Ph(a,r);if(l){let[p,h]=jh([...s],cs(l));if(!p&&!c)return[l,!1,h]}if(hr.has(a)){let p=hr.get(a);if(Dt.has(p)&&(Dt.get(p).delete(a),Dt.get(p).size===0)){Dt.delete(p);for(let[h,b]of Pi)b===p&&Pi.delete(h);for(let h of p.disposables.splice(0))h(p)}}Ze.DEBUG&&console.log("Setting up new context...");let f=il(t,[],r);Object.assign(f,{userConfigPath:i});let[,d]=jh([...s],cs(f));return Pi.set(n,f),hr.set(a,f),Dt.has(f)||Dt.set(f,new Set),Dt.get(f).add(a),[f,!0,d]}var Nh,Zo,Pt,Jo,el,rl,hr,Pi,Dt,Oi=P(()=>{u();ft();la();Ot();Nh=pe(Ra()),Zo=pe(it());Ci();qo();Gn();Kt();fr();Lo();Fr();bh();It();It();Yi();Be();Gi();Bo();os();Ih();Mh();ct();Vo();Pt=Symbol(),Jo={AddVariant:Symbol.for("ADD_VARIANT"),MatchVariant:Symbol.for("MATCH_VARIANT")},el={Base:1<<0,Dynamic:1<<1};rl=new WeakMap;hr=wh,Pi=vh,Dt=es});function nl(r){return r.ignore?[]:r.glob?m.env.ROLLUP_WATCH==="true"?[{type:"dependency",file:r.base}]:[{type:"dir-dependency",dir:r.base,glob:r.glob}]:[{type:"dependency",file:r.base}]}var Hh=P(()=>{u()});function Wh(r,e){return{handler:r,config:e}}var Gh,Qh=P(()=>{u();Wh.withOptions=function(r,e=()=>({})){let t=function(i){return{__options:i,handler:r(i),config:e(i)}};return t.__isOptionsFunction=!0,t.__pluginFunction=r,t.__configFunction=e,t};Gh=Wh});var sl={};Ge(sl,{default:()=>W_});var W_,al=P(()=>{u();Qh();W_=Gh});var Kh=x((z4,Yh)=>{u();var G_=(al(),sl).default,Q_={overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical"},Y_=G_(function({matchUtilities:r,addUtilities:e,theme:t,variants:i}){let n=t("lineClamp");r({"line-clamp":s=>({...Q_,"-webkit-line-clamp":`${s}`})},{values:n}),e([{".line-clamp-none":{"-webkit-line-clamp":"unset"}}],i("lineClamp"))},{theme:{lineClamp:{1:"1",2:"2",3:"3",4:"4",5:"5",6:"6"}},variants:{lineClamp:["responsive"]}});Yh.exports=Y_});function ol(r){r.content.files.length===0&&G.warn("content-problems",["The `content` option in your Tailwind CSS configuration is missing or empty.","Configure your content sources or your generated CSS will be missing styles.","https://tailwindcss.com/docs/content-configuration"]);try{let e=Kh();r.plugins.includes(e)&&(G.warn("line-clamp-in-core",["As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default.","Remove it from the `plugins` array in your configuration to eliminate this warning."]),r.plugins=r.plugins.filter(t=>t!==e))}catch{}return r}var Xh=P(()=>{u();Be()});var Zh,Jh=P(()=>{u();Zh=()=>!1});var ps,em=P(()=>{u();ps={sync:r=>[].concat(r),generateTasks:r=>[{dynamic:!1,base:".",negative:[],positive:[].concat(r),patterns:[].concat(r)}],escapePath:r=>r}});var ll,tm=P(()=>{u();ll=r=>r});var rm,im=P(()=>{u();rm=()=>""});function nm(r){let e=r,t=rm(r);return t!=="."&&(e=r.substr(t.length),e.charAt(0)==="/"&&(e=e.substr(1))),e.substr(0,2)==="./"?e=e.substr(2):e.charAt(0)==="/"&&(e=e.substr(1)),{base:t,glob:e}}var sm=P(()=>{u();im()});var ds=x(Ve=>{u();"use strict";Ve.isInteger=r=>typeof r=="number"?Number.isInteger(r):typeof r=="string"&&r.trim()!==""?Number.isInteger(Number(r)):!1;Ve.find=(r,e)=>r.nodes.find(t=>t.type===e);Ve.exceedsLimit=(r,e,t=1,i)=>i===!1||!Ve.isInteger(r)||!Ve.isInteger(e)?!1:(Number(e)-Number(r))/Number(t)>=i;Ve.escapeNode=(r,e=0,t)=>{let i=r.nodes[e];!i||(t&&i.type===t||i.type==="open"||i.type==="close")&&i.escaped!==!0&&(i.value="\\"+i.value,i.escaped=!0)};Ve.encloseBrace=r=>r.type!=="brace"?!1:r.commas>>0+r.ranges>>0==0?(r.invalid=!0,!0):!1;Ve.isInvalidBrace=r=>r.type!=="brace"?!1:r.invalid===!0||r.dollar?!0:r.commas>>0+r.ranges>>0==0||r.open!==!0||r.close!==!0?(r.invalid=!0,!0):!1;Ve.isOpenOrClose=r=>r.type==="open"||r.type==="close"?!0:r.open===!0||r.close===!0;Ve.reduce=r=>r.reduce((e,t)=>(t.type==="text"&&e.push(t.value),t.type==="range"&&(t.type="text"),e),[]);Ve.flatten=(...r)=>{let e=[],t=i=>{for(let n=0;n{u();"use strict";var am=ds();om.exports=(r,e={})=>{let t=(i,n={})=>{let s=e.escapeInvalid&&am.isInvalidBrace(n),a=i.invalid===!0&&e.escapeInvalid===!0,o="";if(i.value)return(s||a)&&am.isOpenOrClose(i)?"\\"+i.value:i.value;if(i.value)return i.value;if(i.nodes)for(let l of i.nodes)o+=t(l);return o};return t(r)}});var um=x((J4,lm)=>{u();"use strict";lm.exports=function(r){return typeof r=="number"?r-r==0:typeof r=="string"&&r.trim()!==""?Number.isFinite?Number.isFinite(+r):isFinite(+r):!1}});var bm=x((e6,ym)=>{u();"use strict";var fm=um(),Wt=(r,e,t)=>{if(fm(r)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||r===e)return String(r);if(fm(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let i={relaxZeros:!0,...t};typeof i.strictZeros=="boolean"&&(i.relaxZeros=i.strictZeros===!1);let n=String(i.relaxZeros),s=String(i.shorthand),a=String(i.capture),o=String(i.wrap),l=r+":"+e+"="+n+s+a+o;if(Wt.cache.hasOwnProperty(l))return Wt.cache[l].result;let c=Math.min(r,e),f=Math.max(r,e);if(Math.abs(c-f)===1){let v=r+"|"+e;return i.capture?`(${v})`:i.wrap===!1?v:`(?:${v})`}let d=gm(r)||gm(e),p={min:r,max:e,a:c,b:f},h=[],b=[];if(d&&(p.isPadded=d,p.maxLen=String(p.max).length),c<0){let v=f<0?Math.abs(f):1;b=cm(v,Math.abs(c),p,i),c=p.a=0}return f>=0&&(h=cm(c,f,p,i)),p.negatives=b,p.positives=h,p.result=K_(b,h,i),i.capture===!0?p.result=`(${p.result})`:i.wrap!==!1&&h.length+b.length>1&&(p.result=`(?:${p.result})`),Wt.cache[l]=p,p.result};function K_(r,e,t){let i=ul(r,e,"-",!1,t)||[],n=ul(e,r,"",!1,t)||[],s=ul(r,e,"-?",!0,t)||[];return i.concat(s).concat(n).join("|")}function X_(r,e){let t=1,i=1,n=dm(r,t),s=new Set([e]);for(;r<=n&&n<=e;)s.add(n),t+=1,n=dm(r,t);for(n=hm(e+1,i)-1;r1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+mm(o.count),a=c+1;continue}t.isPadded&&(d=rE(c,t,i)),f.string=d+f.pattern+mm(f.count),s.push(f),a=c+1,o=f}return s}function ul(r,e,t,i,n){let s=[];for(let a of r){let{string:o}=a;!i&&!pm(e,"string",o)&&s.push(t+o),i&&pm(e,"string",o)&&s.push(t+o)}return s}function J_(r,e){let t=[];for(let i=0;ie?1:e>r?-1:0}function pm(r,e,t){return r.some(i=>i[e]===t)}function dm(r,e){return Number(String(r).slice(0,-e)+"9".repeat(e))}function hm(r,e){return r-r%Math.pow(10,e)}function mm(r){let[e=0,t=""]=r;return t||e>1?`{${e+(t?","+t:"")}}`:""}function tE(r,e,t){return`[${r}${e-r==1?"":"-"}${e}]`}function gm(r){return/^-?(0+)\d/.test(r)}function rE(r,e,t){if(!e.isPadded)return r;let i=Math.abs(e.maxLen-String(r).length),n=t.relaxZeros!==!1;switch(i){case 0:return"";case 1:return n?"0?":"0";case 2:return n?"0{0,2}":"00";default:return n?`0{0,${i}}`:`0{${i}}`}}Wt.cache={};Wt.clearCache=()=>Wt.cache={};ym.exports=Wt});var pl=x((t6,Cm)=>{u();"use strict";var iE=(Fn(),Bn),wm=bm(),vm=r=>r!==null&&typeof r=="object"&&!Array.isArray(r),nE=r=>e=>r===!0?Number(e):String(e),fl=r=>typeof r=="number"||typeof r=="string"&&r!=="",Ii=r=>Number.isInteger(+r),cl=r=>{let e=`${r}`,t=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++t]==="0";);return t>0},sE=(r,e,t)=>typeof r=="string"||typeof e=="string"?!0:t.stringify===!0,aE=(r,e,t)=>{if(e>0){let i=r[0]==="-"?"-":"";i&&(r=r.slice(1)),r=i+r.padStart(i?e-1:e,"0")}return t===!1?String(r):r},ms=(r,e)=>{let t=r[0]==="-"?"-":"";for(t&&(r=r.slice(1),e--);r.length{r.negatives.sort((o,l)=>ol?1:0),r.positives.sort((o,l)=>ol?1:0);let i=e.capture?"":"?:",n="",s="",a;return r.positives.length&&(n=r.positives.map(o=>ms(String(o),t)).join("|")),r.negatives.length&&(s=`-(${i}${r.negatives.map(o=>ms(String(o),t)).join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,e.wrap?`(${i}${a})`:a},xm=(r,e,t,i)=>{if(t)return wm(r,e,{wrap:!1,...i});let n=String.fromCharCode(r);if(r===e)return n;let s=String.fromCharCode(e);return`[${n}-${s}]`},km=(r,e,t)=>{if(Array.isArray(r)){let i=t.wrap===!0,n=t.capture?"":"?:";return i?`(${n}${r.join("|")})`:r.join("|")}return wm(r,e,t)},Sm=(...r)=>new RangeError("Invalid range arguments: "+iE.inspect(...r)),Am=(r,e,t)=>{if(t.strictRanges===!0)throw Sm([r,e]);return[]},lE=(r,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${r}" to be a number`);return[]},uE=(r,e,t=1,i={})=>{let n=Number(r),s=Number(e);if(!Number.isInteger(n)||!Number.isInteger(s)){if(i.strictRanges===!0)throw Sm([r,e]);return[]}n===0&&(n=0),s===0&&(s=0);let a=n>s,o=String(r),l=String(e),c=String(t);t=Math.max(Math.abs(t),1);let f=cl(o)||cl(l)||cl(c),d=f?Math.max(o.length,l.length,c.length):0,p=f===!1&&sE(r,e,i)===!1,h=i.transform||nE(p);if(i.toRegex&&t===1)return xm(ms(r,d),ms(e,d),!0,i);let b={negatives:[],positives:[]},v=k=>b[k<0?"negatives":"positives"].push(Math.abs(k)),y=[],w=0;for(;a?n>=s:n<=s;)i.toRegex===!0&&t>1?v(n):y.push(aE(h(n,w),d,p)),n=a?n-t:n+t,w++;return i.toRegex===!0?t>1?oE(b,i,d):km(y,null,{wrap:!1,...i}):y},fE=(r,e,t=1,i={})=>{if(!Ii(r)&&r.length>1||!Ii(e)&&e.length>1)return Am(r,e,i);let n=i.transform||(p=>String.fromCharCode(p)),s=`${r}`.charCodeAt(0),a=`${e}`.charCodeAt(0),o=s>a,l=Math.min(s,a),c=Math.max(s,a);if(i.toRegex&&t===1)return xm(l,c,!1,i);let f=[],d=0;for(;o?s>=a:s<=a;)f.push(n(s,d)),s=o?s-t:s+t,d++;return i.toRegex===!0?km(f,null,{wrap:!1,options:i}):f},gs=(r,e,t,i={})=>{if(e==null&&fl(r))return[r];if(!fl(r)||!fl(e))return Am(r,e,i);if(typeof t=="function")return gs(r,e,1,{transform:t});if(vm(t))return gs(r,e,0,t);let n={...i};return n.capture===!0&&(n.wrap=!0),t=t||n.step||1,Ii(t)?Ii(r)&&Ii(e)?uE(r,e,t,n):fE(r,e,Math.max(Math.abs(t),1),n):t!=null&&!vm(t)?lE(t,n):gs(r,e,1,t)};Cm.exports=gs});var Om=x((r6,Em)=>{u();"use strict";var cE=pl(),_m=ds(),pE=(r,e={})=>{let t=(i,n={})=>{let s=_m.isInvalidBrace(n),a=i.invalid===!0&&e.escapeInvalid===!0,o=s===!0||a===!0,l=e.escapeInvalid===!0?"\\":"",c="";if(i.isOpen===!0)return l+i.value;if(i.isClose===!0)return console.log("node.isClose",l,i.value),l+i.value;if(i.type==="open")return o?l+i.value:"(";if(i.type==="close")return o?l+i.value:")";if(i.type==="comma")return i.prev.type==="comma"?"":o?i.value:"|";if(i.value)return i.value;if(i.nodes&&i.ranges>0){let f=_m.reduce(i.nodes),d=cE(...f,{...e,wrap:!1,toRegex:!0,strictZeros:!0});if(d.length!==0)return f.length>1&&d.length>1?`(${d})`:d}if(i.nodes)for(let f of i.nodes)c+=t(f,i);return c};return t(r)};Em.exports=pE});var Pm=x((i6,Rm)=>{u();"use strict";var dE=pl(),Tm=hs(),mr=ds(),Gt=(r="",e="",t=!1)=>{let i=[];if(r=[].concat(r),e=[].concat(e),!e.length)return r;if(!r.length)return t?mr.flatten(e).map(n=>`{${n}}`):e;for(let n of r)if(Array.isArray(n))for(let s of n)i.push(Gt(s,e,t));else for(let s of e)t===!0&&typeof s=="string"&&(s=`{${s}}`),i.push(Array.isArray(s)?Gt(n,s,t):n+s);return mr.flatten(i)},hE=(r,e={})=>{let t=e.rangeLimit===void 0?1e3:e.rangeLimit,i=(n,s={})=>{n.queue=[];let a=s,o=s.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,o=a.queue;if(n.invalid||n.dollar){o.push(Gt(o.pop(),Tm(n,e)));return}if(n.type==="brace"&&n.invalid!==!0&&n.nodes.length===2){o.push(Gt(o.pop(),["{}"]));return}if(n.nodes&&n.ranges>0){let d=mr.reduce(n.nodes);if(mr.exceedsLimit(...d,e.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=dE(...d,e);p.length===0&&(p=Tm(n,e)),o.push(Gt(o.pop(),p)),n.nodes=[];return}let l=mr.encloseBrace(n),c=n.queue,f=n;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,c=f.queue;for(let d=0;d{u();"use strict";Im.exports={MAX_LENGTH:1e4,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` -`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nm=x((s6,Mm)=>{u();"use strict";var mE=hs(),{MAX_LENGTH:qm,CHAR_BACKSLASH:dl,CHAR_BACKTICK:gE,CHAR_COMMA:yE,CHAR_DOT:bE,CHAR_LEFT_PARENTHESES:wE,CHAR_RIGHT_PARENTHESES:vE,CHAR_LEFT_CURLY_BRACE:xE,CHAR_RIGHT_CURLY_BRACE:kE,CHAR_LEFT_SQUARE_BRACKET:$m,CHAR_RIGHT_SQUARE_BRACKET:Lm,CHAR_DOUBLE_QUOTE:SE,CHAR_SINGLE_QUOTE:AE,CHAR_NO_BREAK_SPACE:CE,CHAR_ZERO_WIDTH_NOBREAK_SPACE:_E}=Dm(),EE=(r,e={})=>{if(typeof r!="string")throw new TypeError("Expected a string");let t=e||{},i=typeof t.maxLength=="number"?Math.min(qm,t.maxLength):qm;if(r.length>i)throw new SyntaxError(`Input length (${r.length}), exceeds max characters (${i})`);let n={type:"root",input:r,nodes:[]},s=[n],a=n,o=n,l=0,c=r.length,f=0,d=0,p,h=()=>r[f++],b=v=>{if(v.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&v.type==="text"){o.value+=v.value;return}return a.nodes.push(v),v.parent=a,v.prev=o,o=v,v};for(b({type:"bos"});f0){if(a.ranges>0){a.ranges=0;let v=a.nodes.shift();a.nodes=[v,{type:"text",value:mE(a)}]}b({type:"comma",value:p}),a.commas++;continue}if(p===bE&&d>0&&a.commas===0){let v=a.nodes;if(d===0||v.length===0){b({type:"text",value:p});continue}if(o.type==="dot"){if(a.range=[],o.value+=p,o.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,o.type="text";continue}a.ranges++,a.args=[];continue}if(o.type==="range"){v.pop();let y=v[v.length-1];y.value+=o.value+p,o=y,a.ranges--;continue}b({type:"dot",value:p});continue}b({type:"text",value:p})}do if(a=s.pop(),a.type!=="root"){a.nodes.forEach(w=>{w.nodes||(w.type==="open"&&(w.isOpen=!0),w.type==="close"&&(w.isClose=!0),w.nodes||(w.type="text"),w.invalid=!0)});let v=s[s.length-1],y=v.nodes.indexOf(a);v.nodes.splice(y,1,...a.nodes)}while(s.length>0);return b({type:"eos"}),n};Mm.exports=EE});var jm=x((a6,Fm)=>{u();"use strict";var Bm=hs(),OE=Om(),TE=Pm(),RE=Nm(),Le=(r,e={})=>{let t=[];if(Array.isArray(r))for(let i of r){let n=Le.create(i,e);Array.isArray(n)?t.push(...n):t.push(n)}else t=[].concat(Le.create(r,e));return e&&e.expand===!0&&e.nodupes===!0&&(t=[...new Set(t)]),t};Le.parse=(r,e={})=>RE(r,e);Le.stringify=(r,e={})=>typeof r=="string"?Bm(Le.parse(r,e),e):Bm(r,e);Le.compile=(r,e={})=>(typeof r=="string"&&(r=Le.parse(r,e)),OE(r,e));Le.expand=(r,e={})=>{typeof r=="string"&&(r=Le.parse(r,e));let t=TE(r,e);return e.noempty===!0&&(t=t.filter(Boolean)),e.nodupes===!0&&(t=[...new Set(t)]),t};Le.create=(r,e={})=>r===""||r.length<3?[r]:e.expand!==!0?Le.compile(r,e):Le.expand(r,e);Fm.exports=Le});var Di=x((o6,Wm)=>{u();"use strict";var PE=(et(),Ur),at="\\\\/",zm=`[^${at}]`,yt="\\.",IE="\\+",DE="\\?",ys="\\/",qE="(?=.)",Um="[^/]",hl=`(?:${ys}|$)`,Vm=`(?:^|${ys})`,ml=`${yt}{1,2}${hl}`,$E=`(?!${yt})`,LE=`(?!${Vm}${ml})`,ME=`(?!${yt}{0,1}${hl})`,NE=`(?!${ml})`,BE=`[^.${ys}]`,FE=`${Um}*?`,Hm={DOT_LITERAL:yt,PLUS_LITERAL:IE,QMARK_LITERAL:DE,SLASH_LITERAL:ys,ONE_CHAR:qE,QMARK:Um,END_ANCHOR:hl,DOTS_SLASH:ml,NO_DOT:$E,NO_DOTS:LE,NO_DOT_SLASH:ME,NO_DOTS_SLASH:NE,QMARK_NO_DOT:BE,STAR:FE,START_ANCHOR:Vm},jE={...Hm,SLASH_LITERAL:`[${at}]`,QMARK:zm,STAR:`${zm}*?`,DOTS_SLASH:`${yt}{1,2}(?:[${at}]|$)`,NO_DOT:`(?!${yt})`,NO_DOTS:`(?!(?:^|[${at}])${yt}{1,2}(?:[${at}]|$))`,NO_DOT_SLASH:`(?!${yt}{0,1}(?:[${at}]|$))`,NO_DOTS_SLASH:`(?!${yt}{1,2}(?:[${at}]|$))`,QMARK_NO_DOT:`[^.${at}]`,START_ANCHOR:`(?:^|[${at}])`,END_ANCHOR:`(?:[${at}]|$)`},zE={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Wm.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:zE,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:PE.sep,extglobChars(r){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${r.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(r){return r===!0?jE:Hm}}});var qi=x(Re=>{u();"use strict";var UE=(et(),Ur),VE=m.platform==="win32",{REGEX_BACKSLASH:HE,REGEX_REMOVE_BACKSLASH:WE,REGEX_SPECIAL_CHARS:GE,REGEX_SPECIAL_CHARS_GLOBAL:QE}=Di();Re.isObject=r=>r!==null&&typeof r=="object"&&!Array.isArray(r);Re.hasRegexChars=r=>GE.test(r);Re.isRegexChar=r=>r.length===1&&Re.hasRegexChars(r);Re.escapeRegex=r=>r.replace(QE,"\\$1");Re.toPosixSlashes=r=>r.replace(HE,"/");Re.removeBackslashes=r=>r.replace(WE,e=>e==="\\"?"":e);Re.supportsLookbehinds=()=>{let r=m.version.slice(1).split(".").map(Number);return r.length===3&&r[0]>=9||r[0]===8&&r[1]>=10};Re.isWindows=r=>r&&typeof r.windows=="boolean"?r.windows:VE===!0||UE.sep==="\\";Re.escapeLast=(r,e,t)=>{let i=r.lastIndexOf(e,t);return i===-1?r:r[i-1]==="\\"?Re.escapeLast(r,e,i-1):`${r.slice(0,i)}\\${r.slice(i)}`};Re.removePrefix=(r,e={})=>{let t=r;return t.startsWith("./")&&(t=t.slice(2),e.prefix="./"),t};Re.wrapOutput=(r,e={},t={})=>{let i=t.contains?"":"^",n=t.contains?"":"$",s=`${i}(?:${r})${n}`;return e.negated===!0&&(s=`(?:^(?!${s}).*$)`),s}});var eg=x((u6,Jm)=>{u();"use strict";var Gm=qi(),{CHAR_ASTERISK:gl,CHAR_AT:YE,CHAR_BACKWARD_SLASH:$i,CHAR_COMMA:KE,CHAR_DOT:yl,CHAR_EXCLAMATION_MARK:bl,CHAR_FORWARD_SLASH:Qm,CHAR_LEFT_CURLY_BRACE:wl,CHAR_LEFT_PARENTHESES:vl,CHAR_LEFT_SQUARE_BRACKET:XE,CHAR_PLUS:ZE,CHAR_QUESTION_MARK:Ym,CHAR_RIGHT_CURLY_BRACE:JE,CHAR_RIGHT_PARENTHESES:Km,CHAR_RIGHT_SQUARE_BRACKET:e2}=Di(),Xm=r=>r===Qm||r===$i,Zm=r=>{r.isPrefix!==!0&&(r.depth=r.isGlobstar?1/0:1)},t2=(r,e)=>{let t=e||{},i=r.length-1,n=t.parts===!0||t.scanToEnd===!0,s=[],a=[],o=[],l=r,c=-1,f=0,d=0,p=!1,h=!1,b=!1,v=!1,y=!1,w=!1,k=!1,S=!1,E=!1,T=!1,B=0,N,R,F={value:"",depth:0,isGlob:!1},Y=()=>c>=i,_=()=>l.charCodeAt(c+1),Q=()=>(N=R,l.charCodeAt(++c));for(;c0&&(le=l.slice(0,f),l=l.slice(f),d-=f),U&&b===!0&&d>0?(U=l.slice(0,d),A=l.slice(d)):b===!0?(U="",A=l):U=l,U&&U!==""&&U!=="/"&&U!==l&&Xm(U.charCodeAt(U.length-1))&&(U=U.slice(0,-1)),t.unescape===!0&&(A&&(A=Gm.removeBackslashes(A)),U&&k===!0&&(U=Gm.removeBackslashes(U)));let C={prefix:le,input:r,start:f,base:U,glob:A,isBrace:p,isBracket:h,isGlob:b,isExtglob:v,isGlobstar:y,negated:S,negatedExtglob:E};if(t.tokens===!0&&(C.maxDepth=0,Xm(R)||a.push(F),C.tokens=a),t.parts===!0||t.tokens===!0){let he;for(let V=0;V{u();"use strict";var bs=Di(),Me=qi(),{MAX_LENGTH:ws,POSIX_REGEX_SOURCE:r2,REGEX_NON_SPECIAL_CHARS:i2,REGEX_SPECIAL_CHARS_BACKREF:n2,REPLACEMENTS:tg}=bs,s2=(r,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...r,e);r.sort();let t=`[${r.join("-")}]`;try{new RegExp(t)}catch(i){return r.map(n=>Me.escapeRegex(n)).join("..")}return t},gr=(r,e)=>`Missing ${r}: "${e}" - use "\\\\${e}" to match literal characters`,xl=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");r=tg[r]||r;let t={...e},i=typeof t.maxLength=="number"?Math.min(ws,t.maxLength):ws,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);let s={type:"bos",value:"",output:t.prepend||""},a=[s],o=t.capture?"":"?:",l=Me.isWindows(e),c=bs.globChars(l),f=bs.extglobChars(c),{DOT_LITERAL:d,PLUS_LITERAL:p,SLASH_LITERAL:h,ONE_CHAR:b,DOTS_SLASH:v,NO_DOT:y,NO_DOT_SLASH:w,NO_DOTS_SLASH:k,QMARK:S,QMARK_NO_DOT:E,STAR:T,START_ANCHOR:B}=c,N=$=>`(${o}(?:(?!${B}${$.dot?v:d}).)*?)`,R=t.dot?"":y,F=t.dot?S:E,Y=t.bash===!0?N(t):T;t.capture&&(Y=`(${Y})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let _={input:r,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};r=Me.removePrefix(r,_),n=r.length;let Q=[],U=[],le=[],A=s,C,he=()=>_.index===n-1,V=_.peek=($=1)=>r[_.index+$],Ee=_.advance=()=>r[++_.index]||"",Ie=()=>r.slice(_.index+1),De=($="",ae=0)=>{_.consumed+=$,_.index+=ae},ji=$=>{_.output+=$.output!=null?$.output:$.value,De($.value)},Iv=()=>{let $=1;for(;V()==="!"&&(V(2)!=="("||V(3)==="?");)Ee(),_.start++,$++;return $%2==0?!1:(_.negated=!0,_.start++,!0)},zi=$=>{_[$]++,le.push($)},Ft=$=>{_[$]--,le.pop()},W=$=>{if(A.type==="globstar"){let ae=_.braces>0&&($.type==="comma"||$.type==="brace"),I=$.extglob===!0||Q.length&&($.type==="pipe"||$.type==="paren");$.type!=="slash"&&$.type!=="paren"&&!ae&&!I&&(_.output=_.output.slice(0,-A.output.length),A.type="star",A.value="*",A.output=Y,_.output+=A.output)}if(Q.length&&$.type!=="paren"&&(Q[Q.length-1].inner+=$.value),($.value||$.output)&&ji($),A&&A.type==="text"&&$.type==="text"){A.value+=$.value,A.output=(A.output||"")+$.value;return}$.prev=A,a.push($),A=$},Ui=($,ae)=>{let I={...f[ae],conditions:1,inner:""};I.prev=A,I.parens=_.parens,I.output=_.output;let H=(t.capture?"(":"")+I.open;zi("parens"),W({type:$,value:ae,output:_.output?"":b}),W({type:"paren",extglob:!0,value:Ee(),output:H}),Q.push(I)},Dv=$=>{let ae=$.close+(t.capture?")":""),I;if($.type==="negate"){let H=Y;if($.inner&&$.inner.length>1&&$.inner.includes("/")&&(H=N(t)),(H!==Y||he()||/^\)+$/.test(Ie()))&&(ae=$.close=`)$))${H}`),$.inner.includes("*")&&(I=Ie())&&/^\.[^\\/.]+$/.test(I)){let ce=xl(I,{...e,fastpaths:!1}).output;ae=$.close=`)${ce})${H})`}$.prev.type==="bos"&&(_.negatedExtglob=!0)}W({type:"paren",extglob:!0,value:C,output:ae}),Ft("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(r)){let $=!1,ae=r.replace(n2,(I,H,ce,Ce,ye,Bs)=>Ce==="\\"?($=!0,I):Ce==="?"?H?H+Ce+(ye?S.repeat(ye.length):""):Bs===0?F+(ye?S.repeat(ye.length):""):S.repeat(ce.length):Ce==="."?d.repeat(ce.length):Ce==="*"?H?H+Ce+(ye?Y:""):Y:H?I:`\\${I}`);return $===!0&&(t.unescape===!0?ae=ae.replace(/\\/g,""):ae=ae.replace(/\\+/g,I=>I.length%2==0?"\\\\":I?"\\":"")),ae===r&&t.contains===!0?(_.output=r,_):(_.output=Me.wrapOutput(ae,_,e),_)}for(;!he();){if(C=Ee(),C==="\0")continue;if(C==="\\"){let I=V();if(I==="/"&&t.bash!==!0||I==="."||I===";")continue;if(!I){C+="\\",W({type:"text",value:C});continue}let H=/^\\+/.exec(Ie()),ce=0;if(H&&H[0].length>2&&(ce=H[0].length,_.index+=ce,ce%2!=0&&(C+="\\")),t.unescape===!0?C=Ee():C+=Ee(),_.brackets===0){W({type:"text",value:C});continue}}if(_.brackets>0&&(C!=="]"||A.value==="["||A.value==="[^")){if(t.posix!==!1&&C===":"){let I=A.value.slice(1);if(I.includes("[")&&(A.posix=!0,I.includes(":"))){let H=A.value.lastIndexOf("["),ce=A.value.slice(0,H),Ce=A.value.slice(H+2),ye=r2[Ce];if(ye){A.value=ce+ye,_.backtrack=!0,Ee(),!s.output&&a.indexOf(A)===1&&(s.output=b);continue}}}(C==="["&&V()!==":"||C==="-"&&V()==="]")&&(C=`\\${C}`),C==="]"&&(A.value==="["||A.value==="[^")&&(C=`\\${C}`),t.posix===!0&&C==="!"&&A.value==="["&&(C="^"),A.value+=C,ji({value:C});continue}if(_.quotes===1&&C!=='"'){C=Me.escapeRegex(C),A.value+=C,ji({value:C});continue}if(C==='"'){_.quotes=_.quotes===1?0:1,t.keepQuotes===!0&&W({type:"text",value:C});continue}if(C==="("){zi("parens"),W({type:"paren",value:C});continue}if(C===")"){if(_.parens===0&&t.strictBrackets===!0)throw new SyntaxError(gr("opening","("));let I=Q[Q.length-1];if(I&&_.parens===I.parens+1){Dv(Q.pop());continue}W({type:"paren",value:C,output:_.parens?")":"\\)"}),Ft("parens");continue}if(C==="["){if(t.nobracket===!0||!Ie().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(gr("closing","]"));C=`\\${C}`}else zi("brackets");W({type:"bracket",value:C});continue}if(C==="]"){if(t.nobracket===!0||A&&A.type==="bracket"&&A.value.length===1){W({type:"text",value:C,output:`\\${C}`});continue}if(_.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(gr("opening","["));W({type:"text",value:C,output:`\\${C}`});continue}Ft("brackets");let I=A.value.slice(1);if(A.posix!==!0&&I[0]==="^"&&!I.includes("/")&&(C=`/${C}`),A.value+=C,ji({value:C}),t.literalBrackets===!1||Me.hasRegexChars(I))continue;let H=Me.escapeRegex(A.value);if(_.output=_.output.slice(0,-A.value.length),t.literalBrackets===!0){_.output+=H,A.value=H;continue}A.value=`(${o}${H}|${A.value})`,_.output+=A.value;continue}if(C==="{"&&t.nobrace!==!0){zi("braces");let I={type:"brace",value:C,output:"(",outputIndex:_.output.length,tokensIndex:_.tokens.length};U.push(I),W(I);continue}if(C==="}"){let I=U[U.length-1];if(t.nobrace===!0||!I){W({type:"text",value:C,output:C});continue}let H=")";if(I.dots===!0){let ce=a.slice(),Ce=[];for(let ye=ce.length-1;ye>=0&&(a.pop(),ce[ye].type!=="brace");ye--)ce[ye].type!=="dots"&&Ce.unshift(ce[ye].value);H=s2(Ce,t),_.backtrack=!0}if(I.comma!==!0&&I.dots!==!0){let ce=_.output.slice(0,I.outputIndex),Ce=_.tokens.slice(I.tokensIndex);I.value=I.output="\\{",C=H="\\}",_.output=ce;for(let ye of Ce)_.output+=ye.output||ye.value}W({type:"brace",value:C,output:H}),Ft("braces"),U.pop();continue}if(C==="|"){Q.length>0&&Q[Q.length-1].conditions++,W({type:"text",value:C});continue}if(C===","){let I=C,H=U[U.length-1];H&&le[le.length-1]==="braces"&&(H.comma=!0,I="|"),W({type:"comma",value:C,output:I});continue}if(C==="/"){if(A.type==="dot"&&_.index===_.start+1){_.start=_.index+1,_.consumed="",_.output="",a.pop(),A=s;continue}W({type:"slash",value:C,output:h});continue}if(C==="."){if(_.braces>0&&A.type==="dot"){A.value==="."&&(A.output=d);let I=U[U.length-1];A.type="dots",A.output+=C,A.value+=C,I.dots=!0;continue}if(_.braces+_.parens===0&&A.type!=="bos"&&A.type!=="slash"){W({type:"text",value:C,output:d});continue}W({type:"dot",value:C,output:d});continue}if(C==="?"){if(!(A&&A.value==="(")&&t.noextglob!==!0&&V()==="("&&V(2)!=="?"){Ui("qmark",C);continue}if(A&&A.type==="paren"){let H=V(),ce=C;if(H==="<"&&!Me.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(A.value==="("&&!/[!=<:]/.test(H)||H==="<"&&!/<([!=]|\w+>)/.test(Ie()))&&(ce=`\\${C}`),W({type:"text",value:C,output:ce});continue}if(t.dot!==!0&&(A.type==="slash"||A.type==="bos")){W({type:"qmark",value:C,output:E});continue}W({type:"qmark",value:C,output:S});continue}if(C==="!"){if(t.noextglob!==!0&&V()==="("&&(V(2)!=="?"||!/[!=<:]/.test(V(3)))){Ui("negate",C);continue}if(t.nonegate!==!0&&_.index===0){Iv();continue}}if(C==="+"){if(t.noextglob!==!0&&V()==="("&&V(2)!=="?"){Ui("plus",C);continue}if(A&&A.value==="("||t.regex===!1){W({type:"plus",value:C,output:p});continue}if(A&&(A.type==="bracket"||A.type==="paren"||A.type==="brace")||_.parens>0){W({type:"plus",value:C});continue}W({type:"plus",value:p});continue}if(C==="@"){if(t.noextglob!==!0&&V()==="("&&V(2)!=="?"){W({type:"at",extglob:!0,value:C,output:""});continue}W({type:"text",value:C});continue}if(C!=="*"){(C==="$"||C==="^")&&(C=`\\${C}`);let I=i2.exec(Ie());I&&(C+=I[0],_.index+=I[0].length),W({type:"text",value:C});continue}if(A&&(A.type==="globstar"||A.star===!0)){A.type="star",A.star=!0,A.value+=C,A.output=Y,_.backtrack=!0,_.globstar=!0,De(C);continue}let $=Ie();if(t.noextglob!==!0&&/^\([^?]/.test($)){Ui("star",C);continue}if(A.type==="star"){if(t.noglobstar===!0){De(C);continue}let I=A.prev,H=I.prev,ce=I.type==="slash"||I.type==="bos",Ce=H&&(H.type==="star"||H.type==="globstar");if(t.bash===!0&&(!ce||$[0]&&$[0]!=="/")){W({type:"star",value:C,output:""});continue}let ye=_.braces>0&&(I.type==="comma"||I.type==="brace"),Bs=Q.length&&(I.type==="pipe"||I.type==="paren");if(!ce&&I.type!=="paren"&&!ye&&!Bs){W({type:"star",value:C,output:""});continue}for(;$.slice(0,3)==="/**";){let Vi=r[_.index+4];if(Vi&&Vi!=="/")break;$=$.slice(3),De("/**",3)}if(I.type==="bos"&&he()){A.type="globstar",A.value+=C,A.output=N(t),_.output=A.output,_.globstar=!0,De(C);continue}if(I.type==="slash"&&I.prev.type!=="bos"&&!Ce&&he()){_.output=_.output.slice(0,-(I.output+A.output).length),I.output=`(?:${I.output}`,A.type="globstar",A.output=N(t)+(t.strictSlashes?")":"|$)"),A.value+=C,_.globstar=!0,_.output+=I.output+A.output,De(C);continue}if(I.type==="slash"&&I.prev.type!=="bos"&&$[0]==="/"){let Vi=$[1]!==void 0?"|$":"";_.output=_.output.slice(0,-(I.output+A.output).length),I.output=`(?:${I.output}`,A.type="globstar",A.output=`${N(t)}${h}|${h}${Vi})`,A.value+=C,_.output+=I.output+A.output,_.globstar=!0,De(C+Ee()),W({type:"slash",value:"/",output:""});continue}if(I.type==="bos"&&$[0]==="/"){A.type="globstar",A.value+=C,A.output=`(?:^|${h}|${N(t)}${h})`,_.output=A.output,_.globstar=!0,De(C+Ee()),W({type:"slash",value:"/",output:""});continue}_.output=_.output.slice(0,-A.output.length),A.type="globstar",A.output=N(t),A.value+=C,_.output+=A.output,_.globstar=!0,De(C);continue}let ae={type:"star",value:C,output:Y};if(t.bash===!0){ae.output=".*?",(A.type==="bos"||A.type==="slash")&&(ae.output=R+ae.output),W(ae);continue}if(A&&(A.type==="bracket"||A.type==="paren")&&t.regex===!0){ae.output=C,W(ae);continue}(_.index===_.start||A.type==="slash"||A.type==="dot")&&(A.type==="dot"?(_.output+=w,A.output+=w):t.dot===!0?(_.output+=k,A.output+=k):(_.output+=R,A.output+=R),V()!=="*"&&(_.output+=b,A.output+=b)),W(ae)}for(;_.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing","]"));_.output=Me.escapeLast(_.output,"["),Ft("brackets")}for(;_.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing",")"));_.output=Me.escapeLast(_.output,"("),Ft("parens")}for(;_.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(gr("closing","}"));_.output=Me.escapeLast(_.output,"{"),Ft("braces")}if(t.strictSlashes!==!0&&(A.type==="star"||A.type==="bracket")&&W({type:"maybe_slash",value:"",output:`${h}?`}),_.backtrack===!0){_.output="";for(let $ of _.tokens)_.output+=$.output!=null?$.output:$.value,$.suffix&&(_.output+=$.suffix)}return _};xl.fastpaths=(r,e)=>{let t={...e},i=typeof t.maxLength=="number"?Math.min(ws,t.maxLength):ws,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);r=tg[r]||r;let s=Me.isWindows(e),{DOT_LITERAL:a,SLASH_LITERAL:o,ONE_CHAR:l,DOTS_SLASH:c,NO_DOT:f,NO_DOTS:d,NO_DOTS_SLASH:p,STAR:h,START_ANCHOR:b}=bs.globChars(s),v=t.dot?d:f,y=t.dot?p:f,w=t.capture?"":"?:",k={negated:!1,prefix:""},S=t.bash===!0?".*?":h;t.capture&&(S=`(${S})`);let E=R=>R.noglobstar===!0?S:`(${w}(?:(?!${b}${R.dot?c:a}).)*?)`,T=R=>{switch(R){case"*":return`${v}${l}${S}`;case".*":return`${a}${l}${S}`;case"*.*":return`${v}${S}${a}${l}${S}`;case"*/*":return`${v}${S}${o}${l}${y}${S}`;case"**":return v+E(t);case"**/*":return`(?:${v}${E(t)}${o})?${y}${l}${S}`;case"**/*.*":return`(?:${v}${E(t)}${o})?${y}${S}${a}${l}${S}`;case"**/.*":return`(?:${v}${E(t)}${o})?${a}${l}${S}`;default:{let F=/^(.*?)\.(\w+)$/.exec(R);if(!F)return;let Y=T(F[1]);return Y?Y+a+F[2]:void 0}}},B=Me.removePrefix(r,k),N=T(B);return N&&t.strictSlashes!==!0&&(N+=`${o}?`),N};rg.exports=xl});var sg=x((c6,ng)=>{u();"use strict";var a2=(et(),Ur),o2=eg(),kl=ig(),Sl=qi(),l2=Di(),u2=r=>r&&typeof r=="object"&&!Array.isArray(r),de=(r,e,t=!1)=>{if(Array.isArray(r)){let f=r.map(p=>de(p,e,t));return p=>{for(let h of f){let b=h(p);if(b)return b}return!1}}let i=u2(r)&&r.tokens&&r.input;if(r===""||typeof r!="string"&&!i)throw new TypeError("Expected pattern to be a non-empty string");let n=e||{},s=Sl.isWindows(e),a=i?de.compileRe(r,e):de.makeRe(r,e,!1,!0),o=a.state;delete a.state;let l=()=>!1;if(n.ignore){let f={...e,ignore:null,onMatch:null,onResult:null};l=de(n.ignore,f,t)}let c=(f,d=!1)=>{let{isMatch:p,match:h,output:b}=de.test(f,a,e,{glob:r,posix:s}),v={glob:r,state:o,regex:a,posix:s,input:f,output:b,match:h,isMatch:p};return typeof n.onResult=="function"&&n.onResult(v),p===!1?(v.isMatch=!1,d?v:!1):l(f)?(typeof n.onIgnore=="function"&&n.onIgnore(v),v.isMatch=!1,d?v:!1):(typeof n.onMatch=="function"&&n.onMatch(v),d?v:!0)};return t&&(c.state=o),c};de.test=(r,e,t,{glob:i,posix:n}={})=>{if(typeof r!="string")throw new TypeError("Expected input to be a string");if(r==="")return{isMatch:!1,output:""};let s=t||{},a=s.format||(n?Sl.toPosixSlashes:null),o=r===i,l=o&&a?a(r):r;return o===!1&&(l=a?a(r):r,o=l===i),(o===!1||s.capture===!0)&&(s.matchBase===!0||s.basename===!0?o=de.matchBase(r,e,t,n):o=e.exec(l)),{isMatch:Boolean(o),match:o,output:l}};de.matchBase=(r,e,t,i=Sl.isWindows(t))=>(e instanceof RegExp?e:de.makeRe(e,t)).test(a2.basename(r));de.isMatch=(r,e,t)=>de(e,t)(r);de.parse=(r,e)=>Array.isArray(r)?r.map(t=>de.parse(t,e)):kl(r,{...e,fastpaths:!1});de.scan=(r,e)=>o2(r,e);de.compileRe=(r,e,t=!1,i=!1)=>{if(t===!0)return r.output;let n=e||{},s=n.contains?"":"^",a=n.contains?"":"$",o=`${s}(?:${r.output})${a}`;r&&r.negated===!0&&(o=`^(?!${o}).*$`);let l=de.toRegex(o,e);return i===!0&&(l.state=r),l};de.makeRe=(r,e={},t=!1,i=!1)=>{if(!r||typeof r!="string")throw new TypeError("Expected a non-empty string");let n={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(r[0]==="."||r[0]==="*")&&(n.output=kl.fastpaths(r,e)),n.output||(n=kl(r,e)),de.compileRe(n,e,t,i)};de.toRegex=(r,e)=>{try{let t=e||{};return new RegExp(r,t.flags||(t.nocase?"i":""))}catch(t){if(e&&e.debug===!0)throw t;return/$^/}};de.constants=l2;ng.exports=de});var og=x((p6,ag)=>{u();"use strict";ag.exports=sg()});var dg=x((d6,pg)=>{u();"use strict";var lg=(Fn(),Bn),ug=jm(),ot=og(),Al=qi(),fg=r=>r===""||r==="./",cg=r=>{let e=r.indexOf("{");return e>-1&&r.indexOf("}",e)>-1},oe=(r,e,t)=>{e=[].concat(e),r=[].concat(r);let i=new Set,n=new Set,s=new Set,a=0,o=f=>{s.add(f.output),t&&t.onResult&&t.onResult(f)};for(let f=0;f!i.has(f));if(t&&c.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?e.map(f=>f.replace(/\\/g,"")):e}return c};oe.match=oe;oe.matcher=(r,e)=>ot(r,e);oe.isMatch=(r,e,t)=>ot(e,t)(r);oe.any=oe.isMatch;oe.not=(r,e,t={})=>{e=[].concat(e).map(String);let i=new Set,n=[],s=o=>{t.onResult&&t.onResult(o),n.push(o.output)},a=new Set(oe(r,e,{...t,onResult:s}));for(let o of n)a.has(o)||i.add(o);return[...i]};oe.contains=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${lg.inspect(r)}"`);if(Array.isArray(e))return e.some(i=>oe.contains(r,i,t));if(typeof e=="string"){if(fg(r)||fg(e))return!1;if(r.includes(e)||r.startsWith("./")&&r.slice(2).includes(e))return!0}return oe.isMatch(r,e,{...t,contains:!0})};oe.matchKeys=(r,e,t)=>{if(!Al.isObject(r))throw new TypeError("Expected the first argument to be an object");let i=oe(Object.keys(r),e,t),n={};for(let s of i)n[s]=r[s];return n};oe.some=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=ot(String(n),t);if(i.some(a=>s(a)))return!0}return!1};oe.every=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=ot(String(n),t);if(!i.every(a=>s(a)))return!1}return!0};oe.all=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${lg.inspect(r)}"`);return[].concat(e).every(i=>ot(i,t)(r))};oe.capture=(r,e,t)=>{let i=Al.isWindows(t),s=ot.makeRe(String(r),{...t,capture:!0}).exec(i?Al.toPosixSlashes(e):e);if(s)return s.slice(1).map(a=>a===void 0?"":a)};oe.makeRe=(...r)=>ot.makeRe(...r);oe.scan=(...r)=>ot.scan(...r);oe.parse=(r,e)=>{let t=[];for(let i of[].concat(r||[]))for(let n of ug(String(i),e))t.push(ot.parse(n,e));return t};oe.braces=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!cg(r)?[r]:ug(r,e)};oe.braceExpand=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return oe.braces(r,{...e,expand:!0})};oe.hasBraces=cg;pg.exports=oe});function mg(r,e){let t=e.content.files;t=t.filter(o=>typeof o=="string"),t=t.map(ll);let i=ps.generateTasks(t),n=[],s=[];for(let o of i)n.push(...o.positive.map(l=>gg(l,!1))),s.push(...o.negative.map(l=>gg(l,!0)));let a=[...n,...s];return a=c2(r,a),a=a.flatMap(p2),a=a.map(f2),a}function gg(r,e){let t={original:r,base:r,ignore:e,pattern:r,glob:null};return Zh(r)&&Object.assign(t,nm(r)),t}function f2(r){let e=ll(r.base);return e=ps.escapePath(e),r.pattern=r.glob?`${e}/${r.glob}`:e,r.pattern=r.ignore?`!${r.pattern}`:r.pattern,r}function c2(r,e){let t=[];return r.userConfigPath&&r.tailwindConfig.content.relative&&(t=[me.dirname(r.userConfigPath)]),e.map(i=>(i.base=me.resolve(...t,i.base),i))}function p2(r){let e=[r];try{let t=be.realpathSync(r.base);t!==r.base&&e.push({...r,base:t})}catch{}return e}function yg(r,e,t){let i=r.tailwindConfig.content.files.filter(a=>typeof a.raw=="string").map(({raw:a,extension:o="html"})=>({content:a,extension:o})),[n,s]=h2(e,t);for(let a of n){let o=me.extname(a).slice(1);i.push({file:a,extension:o})}return[i,s]}function d2(r){if(!r.some(s=>s.includes("**")&&!wg.test(s)))return()=>{};let t=[],i=[];for(let s of r){let a=hg.default.matcher(s);wg.test(s)&&i.push(a),t.push(a)}let n=!1;return s=>{if(n||i.some(f=>f(s)))return;let a=t.findIndex(f=>f(s));if(a===-1)return;let o=r[a],l=me.relative(m.cwd(),o);l[0]!=="."&&(l=`./${l}`);let c=bg.find(f=>s.includes(f));c&&(n=!0,G.warn("broad-content-glob-pattern",[`Your \`content\` configuration includes a pattern which looks like it's accidentally matching all of \`${c}\` and can cause serious performance issues.`,`Pattern: \`${l}\``,"See our documentation for recommendations:","https://tailwindcss.com/docs/content-configuration#pattern-recommendations"]))}}function h2(r,e){let t=r.map(o=>o.pattern),i=new Map,n=d2(t),s=new Set;Ze.DEBUG&&console.time("Finding changed files");let a=ps.sync(t,{absolute:!0});for(let o of a){n(o);let l=e.get(o)||-1/0,c=be.statSync(o).mtimeMs;c>l&&(s.add(o),i.set(o,c))}return Ze.DEBUG&&console.timeEnd("Finding changed files"),[s,i]}var hg,bg,wg,vg=P(()=>{u();ft();et();Jh();em();tm();sm();It();Be();hg=pe(dg());bg=["node_modules"],wg=new RegExp(`(${bg.map(r=>String.raw`\b${r}\b`).join("|")})`)});function xg(){}var kg=P(()=>{u()});function b2(r,e){for(let t of e){let i=`${r}${t}`;if(be.existsSync(i)&&be.statSync(i).isFile())return i}for(let t of e){let i=`${r}/index${t}`;if(be.existsSync(i))return i}return null}function*Sg(r,e,t,i=me.extname(r)){let n=b2(me.resolve(e,r),m2.includes(i)?g2:y2);if(n===null||t.has(n))return;t.add(n),yield n,e=me.dirname(n),i=me.extname(n);let s=be.readFileSync(n,"utf-8");for(let a of[...s.matchAll(/import[\s\S]*?['"](.{3,}?)['"]/gi),...s.matchAll(/import[\s\S]*from[\s\S]*?['"](.{3,}?)['"]/gi),...s.matchAll(/require\(['"`](.+)['"`]\)/gi)])!a[1].startsWith(".")||(yield*Sg(a[1],e,t,i))}function Cl(r){return r===null?new Set:new Set(Sg(r,me.dirname(r),new Set))}var m2,g2,y2,Ag=P(()=>{u();ft();et();m2=[".js",".cjs",".mjs"],g2=["",".js",".cjs",".mjs",".ts",".cts",".mts",".jsx",".tsx"],y2=["",".ts",".cts",".mts",".tsx",".js",".cjs",".mjs",".jsx"]});function w2(r,e){if(_l.has(r))return _l.get(r);let t=mg(r,e);return _l.set(r,t).get(r)}function v2(r){let e=aa(r);if(e!==null){let[i,n,s,a]=_g.get(e)||[],o=Cl(e),l=!1,c=new Map;for(let p of o){let h=be.statSync(p).mtimeMs;c.set(p,h),(!a||!a.has(p)||h>a.get(p))&&(l=!0)}if(!l)return[i,e,n,s];for(let p of o)delete hf.cache[p];let f=ol(zr(xg(e))),d=Wi(f);return _g.set(e,[f,d,o,c]),[f,e,d,o]}let t=zr(r?.config??r??{});return t=ol(t),[t,null,Wi(t),[]]}function El(r){return({tailwindDirectives:e,registerDependency:t})=>(i,n)=>{let[s,a,o,l]=v2(r),c=new Set(l);if(e.size>0){c.add(n.opts.from);for(let b of n.messages)b.type==="dependency"&&c.add(b.file)}let[f,,d]=Vh(i,n,s,a,o,c),p=cs(f),h=w2(f,s);if(e.size>0){for(let y of h)for(let w of nl(y))t(w);let[b,v]=yg(f,h,p);for(let y of b)f.changedContent.push(y);for(let[y,w]of v.entries())d.set(y,w)}for(let b of l)t({type:"dependency",file:b});for(let[b,v]of d.entries())p.set(b,v);return f}}var Cg,_g,_l,Eg=P(()=>{u();ft();Cg=pe(Fs());wf();sa();oc();Oi();Hh();Xh();vg();kg();Ag();_g=new Cg.default({maxSize:100}),_l=new WeakMap});function Ol(r){let e=new Set,t=new Set,i=new Set;if(r.walkAtRules(n=>{n.name==="apply"&&i.add(n),n.name==="import"&&(n.params==='"tailwindcss/base"'||n.params==="'tailwindcss/base'"?(n.name="tailwind",n.params="base"):n.params==='"tailwindcss/components"'||n.params==="'tailwindcss/components'"?(n.name="tailwind",n.params="components"):n.params==='"tailwindcss/utilities"'||n.params==="'tailwindcss/utilities'"?(n.name="tailwind",n.params="utilities"):(n.params==='"tailwindcss/screens"'||n.params==="'tailwindcss/screens'"||n.params==='"tailwindcss/variants"'||n.params==="'tailwindcss/variants'")&&(n.name="tailwind",n.params="variants")),n.name==="tailwind"&&(n.params==="screens"&&(n.params="variants"),e.add(n.params)),["layer","responsive","variants"].includes(n.name)&&(["responsive","variants"].includes(n.name)&&G.warn(`${n.name}-at-rule-deprecated`,[`The \`@${n.name}\` directive has been deprecated in Tailwind CSS v3.0.`,"Use `@layer utilities` or `@layer components` instead.","https://tailwindcss.com/docs/upgrade-guide#replace-variants-with-layer"]),t.add(n))}),!e.has("base")||!e.has("components")||!e.has("utilities")){for(let n of t)if(n.name==="layer"&&["base","components","utilities"].includes(n.params)){if(!e.has(n.params))throw n.error(`\`@layer ${n.params}\` is used but no matching \`@tailwind ${n.params}\` directive is present.`)}else if(n.name==="responsive"){if(!e.has("utilities"))throw n.error("`@responsive` is used but `@tailwind utilities` is missing.")}else if(n.name==="variants"&&!e.has("utilities"))throw n.error("`@variants` is used but `@tailwind utilities` is missing.")}return{tailwindDirectives:e,applyDirectives:i}}var Og=P(()=>{u();Be()});function Qt(r,e=void 0,t=void 0){return r.map(i=>{let n=i.clone();return t!==void 0&&(n.raws.tailwind={...n.raws.tailwind,...t}),e!==void 0&&Tg(n,s=>{if(s.raws.tailwind?.preserveSource===!0&&s.source)return!1;s.source=e}),n})}function Tg(r,e){e(r)!==!1&&r.each?.(t=>Tg(t,e))}var Rg=P(()=>{u()});function Tl(r){return r=Array.isArray(r)?r:[r],r=r.map(e=>e instanceof RegExp?e.source:e),r.join("")}function Ne(r){return new RegExp(Tl(r),"g")}function qt(r){return`(?:${r.map(Tl).join("|")})`}function Rl(r){return`(?:${Tl(r)})?`}function Ig(r){return r&&x2.test(r)?r.replace(Pg,"\\$&"):r||""}var Pg,x2,Dg=P(()=>{u();Pg=/[\\^$.*+?()[\]{}|]/g,x2=RegExp(Pg.source)});function qg(r){let e=Array.from(k2(r));return t=>{let i=[];for(let n of e)for(let s of t.match(n)??[])i.push(C2(s));for(let n of i.slice()){let s=ve(n,".");for(let a=0;a=s.length-1){i.push(o);continue}let l=Number(s[a+1]);isNaN(l)?i.push(o):a++}}return i}}function*k2(r){let e=r.tailwindConfig.separator,t=r.tailwindConfig.prefix!==""?Rl(Ne([/-?/,Ig(r.tailwindConfig.prefix)])):"",i=qt([/\[[^\s:'"`]+:[^\s\[\]]+\]/,/\[[^\s:'"`\]]+:[^\s]+?\[[^\s]+\][^\s]+?\]/,Ne([qt([/-?(?:\w+)/,/@(?:\w+)/]),Rl(qt([Ne([qt([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s:\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\><$]*)?/]),Ne([qt([/-(?:\w+-)*\['[^\s]+'\]/,/-(?:\w+-)*\["[^\s]+"\]/,/-(?:\w+-)*\[`[^\s]+`\]/,/-(?:\w+-)*\[(?:[^\s\[\]]+\[[^\s\[\]]+\])*[^\s\[\]]+\]/]),/(?![{([]])/,/(?:\/[^\s'"`\\$]*)?/]),/[-\/][^\s'"`\\$={><]*/]))])]),n=[qt([Ne([/@\[[^\s"'`]+\](\/[^\s"'`]+)?/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]\/[\w_-]+/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s"'`]+\]/,e]),Ne([/[^\s"'`\[\\]+/,e])]),qt([Ne([/([^\s"'`\[\\]+-)?\[[^\s`]+\]\/[\w_-]+/,e]),Ne([/([^\s"'`\[\\]+-)?\[[^\s`]+\]/,e]),Ne([/[^\s`\[\\]+/,e])])];for(let s of n)yield Ne(["((?=((",s,")+))\\2)?",/!?/,t,i]);yield/[^<>"'`\s.(){}[\]#=%$][^<>"'`\s(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g}function C2(r){if(!r.includes("-["))return r;let e=0,t=[],i=r.matchAll(S2);i=Array.from(i).flatMap(n=>{let[,...s]=n;return s.map((a,o)=>Object.assign([],n,{index:n.index+o,0:a}))});for(let n of i){let s=n[0],a=t[t.length-1];if(s===a?t.pop():(s==="'"||s==='"'||s==="`")&&t.push(s),!a){if(s==="["){e++;continue}else if(s==="]"){e--;continue}if(e<0)return r.substring(0,n.index-1);if(e===0&&!A2.test(s))return r.substring(0,n.index)}}return r}var S2,A2,$g=P(()=>{u();Dg();zt();S2=/([\[\]'"`])([^\[\]'"`])?/g,A2=/[^"'`\s<>\]]+/});function _2(r,e){let t=r.tailwindConfig.content.extract;return t[e]||t.DEFAULT||Mg[e]||Mg.DEFAULT(r)}function E2(r,e){let t=r.content.transform;return t[e]||t.DEFAULT||Ng[e]||Ng.DEFAULT}function O2(r,e,t,i){Li.has(e)||Li.set(e,new Lg.default({maxSize:25e3}));for(let n of r.split(` -`))if(n=n.trim(),!i.has(n))if(i.add(n),Li.get(e).has(n))for(let s of Li.get(e).get(n))t.add(s);else{let s=e(n).filter(o=>o!=="!*"),a=new Set(s);for(let o of a)t.add(o);Li.get(e).set(n,a)}}function T2(r,e){let t=e.offsets.sort(r),i={base:new Set,defaults:new Set,components:new Set,utilities:new Set,variants:new Set};for(let[n,s]of t)i[n.layer].add(s);return i}function Pl(r){return async e=>{let t={base:null,components:null,utilities:null,variants:null};if(e.walkAtRules(y=>{y.name==="tailwind"&&Object.keys(t).includes(y.params)&&(t[y.params]=y)}),Object.values(t).every(y=>y===null))return e;let i=new Set([...r.candidates??[],gt]),n=new Set;bt.DEBUG&&console.time("Reading changed files");let s=[];for(let y of r.changedContent){let w=E2(r.tailwindConfig,y.extension),k=_2(r,y.extension);s.push([y,{transformer:w,extractor:k}])}let a=500;for(let y=0;y{S=k?await be.promises.readFile(k,"utf8"):S,O2(E(S),T,i,n)}))}bt.DEBUG&&console.timeEnd("Reading changed files");let o=r.classCache.size;bt.DEBUG&&console.time("Generate rules"),bt.DEBUG&&console.time("Sorting candidates");let l=new Set([...i].sort((y,w)=>y===w?0:y{let w=y.raws.tailwind?.parentLayer;return w==="components"?t.components!==null:w==="utilities"?t.utilities!==null:!0});t.variants?(t.variants.before(Qt(b,t.variants.source,{layer:"variants"})),t.variants.remove()):b.length>0&&e.append(Qt(b,e.source,{layer:"variants"})),e.source.end=e.source.end??e.source.start;let v=b.some(y=>y.raws.tailwind?.parentLayer==="utilities");t.utilities&&p.size===0&&!v&&G.warn("content-problems",["No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration.","https://tailwindcss.com/docs/content-configuration"]),bt.DEBUG&&(console.log("Potential classes: ",i.size),console.log("Active contexts: ",es.size)),r.changedContent=[],e.walkAtRules("layer",y=>{Object.keys(t).includes(y.params)&&y.remove()})}}var Lg,bt,Mg,Ng,Li,Bg=P(()=>{u();ft();Lg=pe(Fs());It();os();Be();Rg();$g();bt=Ze,Mg={DEFAULT:qg},Ng={DEFAULT:r=>r,svelte:r=>r.replace(/(?:^|\s)class:/g," ")};Li=new WeakMap});function xs(r){let e=new Map;ee.root({nodes:[r.clone()]}).walkRules(s=>{(0,vs.default)(a=>{a.walkClasses(o=>{let l=o.parent.toString(),c=e.get(l);c||e.set(l,c=new Set),c.add(o.value)})}).processSync(s.selector)});let i=Array.from(e.values(),s=>Array.from(s)),n=i.flat();return Object.assign(n,{groups:i})}function Il(r){return R2.astSync(r)}function Fg(r,e){let t=new Set;for(let i of r)t.add(i.split(e).pop());return Array.from(t)}function jg(r,e){let t=r.tailwindConfig.prefix;return typeof t=="function"?t(e):t+e}function*zg(r){for(yield r;r.parent;)yield r.parent,r=r.parent}function P2(r,e={}){let t=r.nodes;r.nodes=[];let i=r.clone(e);return r.nodes=t,i}function I2(r){for(let e of zg(r))if(r!==e){if(e.type==="root")break;r=P2(e,{nodes:[r]})}return r}function D2(r,e){let t=new Map;return r.walkRules(i=>{for(let a of zg(i))if(a.raws.tailwind?.layer!==void 0)return;let n=I2(i),s=e.offsets.create("user");for(let a of xs(i)){let o=t.get(a)||[];t.set(a,o),o.push([{layer:"user",sort:s,important:!1},n])}}),t}function q2(r,e){for(let t of r){if(e.notClassCache.has(t)||e.applyClassCache.has(t))continue;if(e.classCache.has(t)){e.applyClassCache.set(t,e.classCache.get(t).map(([n,s])=>[n,s.clone()]));continue}let i=Array.from(Yo(t,e));if(i.length===0){e.notClassCache.add(t);continue}e.applyClassCache.set(t,i)}return e.applyClassCache}function $2(r){let e=null;return{get:t=>(e=e||r(),e.get(t)),has:t=>(e=e||r(),e.has(t))}}function L2(r){return{get:e=>r.flatMap(t=>t.get(e)||[]),has:e=>r.some(t=>t.has(e))}}function Ug(r){let e=r.split(/[\s\t\n]+/g);return e[e.length-1]==="!important"?[e.slice(0,-1),!0]:[e,!1]}function Vg(r,e,t){let i=new Set,n=[];if(r.walkAtRules("apply",l=>{let[c]=Ug(l.params);for(let f of c)i.add(f);n.push(l)}),n.length===0)return;let s=L2([t,q2(i,e)]);function a(l,c,f){let d=Il(l),p=Il(c),b=Il(`.${Te(f)}`).nodes[0].nodes[0];return d.each(v=>{let y=new Set;p.each(w=>{let k=!1;w=w.clone(),w.walkClasses(S=>{S.value===b.value&&(k||(S.replaceWith(...v.nodes.map(E=>E.clone())),y.add(w),k=!0))})});for(let w of y){let k=[[]];for(let S of w.nodes)S.type==="combinator"?(k.push(S),k.push([])):k[k.length-1].push(S);w.nodes=[];for(let S of k)Array.isArray(S)&&S.sort((E,T)=>E.type==="tag"&&T.type==="class"?-1:E.type==="class"&&T.type==="tag"?1:E.type==="class"&&T.type==="pseudo"&&T.value.startsWith("::")?-1:E.type==="pseudo"&&E.value.startsWith("::")&&T.type==="class"?1:0),w.nodes=w.nodes.concat(S)}v.replaceWith(...y)}),d.toString()}let o=new Map;for(let l of n){let[c]=o.get(l.parent)||[[],l.source];o.set(l.parent,[c,l.source]);let[f,d]=Ug(l.params);if(l.parent.type==="atrule"){if(l.parent.name==="screen"){let p=l.parent.params;throw l.error(`@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${f.map(h=>`${p}:${h}`).join(" ")} instead.`)}throw l.error(`@apply is not supported within nested at-rules like @${l.parent.name}. You can fix this by un-nesting @${l.parent.name}.`)}for(let p of f){if([jg(e,"group"),jg(e,"peer")].includes(p))throw l.error(`@apply should not be used with the '${p}' utility`);if(!s.has(p))throw l.error(`The \`${p}\` class does not exist. If \`${p}\` is a custom class, make sure it is defined within a \`@layer\` directive.`);let h=s.get(p);for(let[,b]of h)b.type!=="atrule"&&b.walkRules(()=>{throw l.error([`The \`${p}\` class cannot be used with \`@apply\` because \`@apply\` does not currently support nested CSS.`,"Rewrite the selector without nesting or configure the `tailwindcss/nesting` plugin:","https://tailwindcss.com/docs/using-with-preprocessors#nesting"].join(` -`))});c.push([p,d,h])}}for(let[l,[c,f]]of o){let d=[];for(let[h,b,v]of c){let y=[h,...Fg([h],e.tailwindConfig.separator)];for(let[w,k]of v){let S=xs(l),E=xs(k);if(E=E.groups.filter(R=>R.some(F=>y.includes(F))).flat(),E=E.concat(Fg(E,e.tailwindConfig.separator)),S.some(R=>E.includes(R)))throw k.error(`You cannot \`@apply\` the \`${h}\` utility here because it creates a circular dependency.`);let B=ee.root({nodes:[k.clone()]});B.walk(R=>{R.source=f}),(k.type!=="atrule"||k.type==="atrule"&&k.name!=="keyframes")&&B.walkRules(R=>{if(!xs(R).some(U=>U===h)){R.remove();return}let F=typeof e.tailwindConfig.important=="string"?e.tailwindConfig.important:null,_=l.raws.tailwind!==void 0&&F&&l.selector.indexOf(F)===0?l.selector.slice(F.length):l.selector;_===""&&(_=l.selector),R.selector=a(_,R.selector,h),F&&_!==l.selector&&(R.selector=is(R.selector,F)),R.walkDecls(U=>{U.important=w.important||b});let Q=(0,vs.default)().astSync(R.selector);Q.each(U=>pr(U)),R.selector=Q.toString()}),!!B.nodes[0]&&d.push([w.sort,B.nodes[0]])}}let p=e.offsets.sort(d).map(h=>h[1]);l.after(p)}for(let l of n)l.parent.nodes.length>1?l.remove():l.parent.remove();Vg(r,e,t)}function Dl(r){return e=>{let t=$2(()=>D2(e,r));Vg(e,r,t)}}var vs,R2,Hg=P(()=>{u();Ot();vs=pe(it());os();fr();Wo();ts();R2=(0,vs.default)()});var Wg=x((nq,ks)=>{u();(function(){"use strict";function r(i,n,s){if(!i)return null;r.caseSensitive||(i=i.toLowerCase());var a=r.threshold===null?null:r.threshold*i.length,o=r.thresholdAbsolute,l;a!==null&&o!==null?l=Math.min(a,o):a!==null?l=a:o!==null?l=o:l=null;var c,f,d,p,h,b=n.length;for(h=0;hs)return s+1;var l=[],c,f,d,p,h;for(c=0;c<=o;c++)l[c]=[c];for(f=0;f<=a;f++)l[0][f]=f;for(c=1;c<=o;c++){for(d=e,p=1,c>s&&(p=c-s),h=o+1,h>s+c&&(h=s+c),f=1;f<=a;f++)fh?l[c][f]=s+1:n.charAt(c-1)===i.charAt(f-1)?l[c][f]=l[c-1][f-1]:l[c][f]=Math.min(l[c-1][f-1]+1,Math.min(l[c][f-1]+1,l[c-1][f]+1)),l[c][f]s)return s+1}return l[o][a]}})()});var Qg=x((sq,Gg)=>{u();var ql="(".charCodeAt(0),$l=")".charCodeAt(0),Ss="'".charCodeAt(0),Ll='"'.charCodeAt(0),Ml="\\".charCodeAt(0),yr="/".charCodeAt(0),Nl=",".charCodeAt(0),Bl=":".charCodeAt(0),As="*".charCodeAt(0),M2="u".charCodeAt(0),N2="U".charCodeAt(0),B2="+".charCodeAt(0),F2=/^[a-f0-9?-]+$/i;Gg.exports=function(r){for(var e=[],t=r,i,n,s,a,o,l,c,f,d=0,p=t.charCodeAt(d),h=t.length,b=[{nodes:e}],v=0,y,w="",k="",S="";d{u();Yg.exports=function r(e,t,i){var n,s,a,o;for(n=0,s=e.length;n{u();function Xg(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Zg(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Zg(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Xg(r[i],e)+t;return t}return Xg(r,e)}Jg.exports=Zg});var ry=x((lq,ty)=>{u();var Cs="-".charCodeAt(0),_s="+".charCodeAt(0),Fl=".".charCodeAt(0),j2="e".charCodeAt(0),z2="E".charCodeAt(0);function U2(r){var e=r.charCodeAt(0),t;if(e===_s||e===Cs){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===Fl&&i>=48&&i<=57}return e===Fl?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}ty.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!U2(r))return!1;for(i=r.charCodeAt(e),(i===_s||i===Cs)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===Fl&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),s=r.charCodeAt(e+2),(i===j2||i===z2)&&(n>=48&&n<=57||(n===_s||n===Cs)&&s>=48&&s<=57))for(e+=n===_s||n===Cs?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var ay=x((uq,sy)=>{u();var V2=Qg(),iy=Kg(),ny=ey();function $t(r){return this instanceof $t?(this.nodes=V2(r),this):new $t(r)}$t.prototype.toString=function(){return Array.isArray(this.nodes)?ny(this.nodes):""};$t.prototype.walk=function(r,e){return iy(this.nodes,r,e),this};$t.unit=ry();$t.walk=iy;$t.stringify=ny;sy.exports=$t});function zl(r){return typeof r=="object"&&r!==null}function H2(r,e){let t=kt(e);do if(t.pop(),(0,Mi.default)(r,t)!==void 0)break;while(t.length);return t.length?t:void 0}function br(r){return typeof r=="string"?r:r.reduce((e,t,i)=>t.includes(".")?`${e}[${t}]`:i===0?t:`${e}.${t}`,"")}function ly(r){return r.map(e=>`'${e}'`).join(", ")}function uy(r){return ly(Object.keys(r))}function Ul(r,e,t,i={}){let n=Array.isArray(e)?br(e):e.replace(/^['"]+|['"]+$/g,""),s=Array.isArray(e)?e:kt(n),a=(0,Mi.default)(r.theme,s,t);if(a===void 0){let l=`'${n}' does not exist in your theme config.`,c=s.slice(0,-1),f=(0,Mi.default)(r.theme,c);if(zl(f)){let d=Object.keys(f).filter(h=>Ul(r,[...c,h]).isValid),p=(0,oy.default)(s[s.length-1],d);p?l+=` Did you mean '${br([...c,p])}'?`:d.length>0&&(l+=` '${br(c)}' has the following valid keys: ${ly(d)}`)}else{let d=H2(r.theme,n);if(d){let p=(0,Mi.default)(r.theme,d);zl(p)?l+=` '${br(d)}' has the following keys: ${uy(p)}`:l+=` '${br(d)}' is not an object.`}else l+=` Your theme has the following top-level keys: ${uy(r.theme)}`}return{isValid:!1,error:l}}if(!(typeof a=="string"||typeof a=="number"||typeof a=="function"||a instanceof String||a instanceof Number||Array.isArray(a))){let l=`'${n}' was found but does not resolve to a string.`;if(zl(a)){let c=Object.keys(a).filter(f=>Ul(r,[...s,f]).isValid);c.length&&(l+=` Did you mean something like '${br([...s,c[0]])}'?`)}return{isValid:!1,error:l}}let[o]=s;return{isValid:!0,value:mt(o)(a,i)}}function W2(r,e,t){e=e.map(n=>fy(r,n,t));let i=[""];for(let n of e)n.type==="div"&&n.value===","?i.push(""):i[i.length-1]+=jl.default.stringify(n);return i}function fy(r,e,t){if(e.type==="function"&&t[e.value]!==void 0){let i=W2(r,e.nodes,t);e.type="word",e.value=t[e.value](r,...i)}return e}function G2(r,e,t){return Object.keys(t).some(n=>e.includes(`${n}(`))?(0,jl.default)(e).walk(n=>{fy(r,n,t)}).toString():e}function*Y2(r){r=r.replace(/^['"]+|['"]+$/g,"");let e=r.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/),t;yield[r,void 0],e&&(r=e[1],t=e[2],yield[r,t])}function K2(r,e,t){let i=Array.from(Y2(e)).map(([n,s])=>Object.assign(Ul(r,n,t,{opacityValue:s}),{resolvedPath:n,alpha:s}));return i.find(n=>n.isValid)??i[0]}function cy(r){let e=r.tailwindConfig,t={theme:(i,n,...s)=>{let{isValid:a,value:o,error:l,alpha:c}=K2(e,n,s.length?s:void 0);if(!a){let p=i.parent,h=p?.raws.tailwind?.candidate;if(p&&h!==void 0){r.markInvalidUtilityNode(p),p.remove(),G.warn("invalid-theme-key-in-class",[`The utility \`${h}\` contains an invalid theme value and was not generated.`]);return}throw i.error(l)}let f=Xt(o),d=f!==void 0&&typeof f=="function";return(c!==void 0||d)&&(c===void 0&&(c=1),o=Je(f,c,f)),o},screen:(i,n)=>{n=n.replace(/^['"]+/g,"").replace(/['"]+$/g,"");let a=Rt(e.theme.screens).find(({name:o})=>o===n);if(!a)throw i.error(`The '${n}' screen does not exist in your theme.`);return Tt(a)}};return i=>{i.walk(n=>{let s=Q2[n.type];s!==void 0&&(n[s]=G2(n,n[s],t))})}}var Mi,oy,jl,Q2,py=P(()=>{u();Mi=pe(Ra()),oy=pe(Wg());Ci();jl=pe(ay());Zn();Yn();Yi();Lr();Fr();Be();Q2={atrule:"params",decl:"value"}});function dy({tailwindConfig:{theme:r}}){return function(e){e.walkAtRules("screen",t=>{let i=t.params,s=Rt(r.screens).find(({name:a})=>a===i);if(!s)throw t.error(`No \`${i}\` screen found.`);t.name="media",t.params=Tt(s)})}}var hy=P(()=>{u();Zn();Yn()});function X2(r){let e=r.filter(o=>o.type!=="pseudo"||o.nodes.length>0?!0:o.value.startsWith("::")||[":before",":after",":first-line",":first-letter"].includes(o.value)).reverse(),t=new Set(["tag","class","id","attribute"]),i=e.findIndex(o=>t.has(o.type));if(i===-1)return e.reverse().join("").trim();let n=e[i],s=my[n.type]?my[n.type](n):n;e=e.slice(0,i);let a=e.findIndex(o=>o.type==="combinator"&&o.value===">");return a!==-1&&(e.splice(0,a),e.unshift(Es.default.universal())),[s,...e.reverse()].join("").trim()}function J2(r){return Vl.has(r)||Vl.set(r,Z2.transformSync(r)),Vl.get(r)}function Hl({tailwindConfig:r}){return e=>{let t=new Map,i=new Set;if(e.walkAtRules("defaults",n=>{if(n.nodes&&n.nodes.length>0){i.add(n);return}let s=n.params;t.has(s)||t.set(s,new Set),t.get(s).add(n.parent),n.remove()}),we(r,"optimizeUniversalDefaults"))for(let n of i){let s=new Map,a=t.get(n.params)??[];for(let o of a)for(let l of J2(o.selector)){let c=l.includes(":-")||l.includes("::-")||l.includes(":has")?l:"__DEFAULT__",f=s.get(c)??new Set;s.set(c,f),f.add(l)}if(s.size===0){n.remove();continue}for(let[,o]of s){let l=ee.rule({source:n.source});l.selectors=[...o],l.append(n.nodes.map(c=>c.clone())),n.before(l)}n.remove()}else if(i.size){let n=ee.rule({selectors:["*","::before","::after"]});for(let a of i)n.append(a.nodes),n.parent||a.before(n),n.source||(n.source=a.source),a.remove();let s=n.clone({selectors:["::backdrop"]});n.after(s)}}}var Es,my,Z2,Vl,gy=P(()=>{u();Ot();Es=pe(it());ct();my={id(r){return Es.default.attribute({attribute:"id",operator:"=",value:r.value,quoteMark:'"'})}};Z2=(0,Es.default)(r=>r.map(e=>{let t=e.split(i=>i.type==="combinator"&&i.value===" ").pop();return X2(t)})),Vl=new Map});function Wl(){function r(e){let t=null;e.each(i=>{if(!eO.has(i.type)){t=null;return}if(t===null){t=i;return}let n=yy[i.type];i.type==="atrule"&&i.name==="font-face"?t=i:n.every(s=>(i[s]??"").replace(/\s+/g," ")===(t[s]??"").replace(/\s+/g," "))?(i.nodes&&t.append(i.nodes),i.remove()):t=i}),e.each(i=>{i.type==="atrule"&&r(i)})}return e=>{r(e)}}var yy,eO,by=P(()=>{u();yy={atrule:["name","params"],rule:["selector"]},eO=new Set(Object.keys(yy))});function Gl(){return r=>{r.walkRules(e=>{let t=new Map,i=new Set([]),n=new Map;e.walkDecls(s=>{if(s.parent===e){if(t.has(s.prop)){if(t.get(s.prop).value===s.value){i.add(t.get(s.prop)),t.set(s.prop,s);return}n.has(s.prop)||n.set(s.prop,new Set),n.get(s.prop).add(t.get(s.prop)),n.get(s.prop).add(s)}t.set(s.prop,s)}});for(let s of i)s.remove();for(let s of n.values()){let a=new Map;for(let o of s){let l=rO(o.value);l!==null&&(a.has(l)||a.set(l,new Set),a.get(l).add(o))}for(let o of a.values()){let l=Array.from(o).slice(0,-1);for(let c of l)c.remove()}}})}}function rO(r){let e=/^-?\d*.?\d+([\w%]+)?$/g.exec(r);return e?e[1]??tO:null}var tO,wy=P(()=>{u();tO=Symbol("unitless-number")});function iO(r){if(!r.walkAtRules)return;let e=new Set;if(r.walkAtRules("apply",t=>{e.add(t.parent)}),e.size!==0)for(let t of e){let i=[],n=[];for(let s of t.nodes)s.type==="atrule"&&s.name==="apply"?(n.length>0&&(i.push(n),n=[]),i.push([s])):n.push(s);if(n.length>0&&i.push(n),i.length!==1){for(let s of[...i].reverse()){let a=t.clone({nodes:[]});a.append(s),t.after(a)}t.remove()}}}function Os(){return r=>{iO(r)}}var vy=P(()=>{u()});function Ts(r){return async function(e,t){let{tailwindDirectives:i,applyDirectives:n}=Ol(e);Os()(e,t);let s=r({tailwindDirectives:i,applyDirectives:n,registerDependency(a){t.messages.push({plugin:"tailwindcss",parent:t.opts.from,...a})},createContext(a,o){return il(a,o,e)}})(e,t);if(s.tailwindConfig.separator==="-")throw new Error("The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead.");Rf(s.tailwindConfig),await Pl(s)(e,t),Os()(e,t),Dl(s)(e,t),cy(s)(e,t),dy(s)(e,t),Hl(s)(e,t),Wl(s)(e,t),Gl(s)(e,t)}}var xy=P(()=>{u();Og();Bg();Hg();py();hy();gy();by();wy();vy();Oi();ct()});function ky(r,e){let t=null,i=null;return r.walkAtRules("config",n=>{if(i=n.source?.input.file??e.opts.from??null,i===null)throw n.error("The `@config` directive cannot be used without setting `from` in your PostCSS config.");if(t)throw n.error("Only one `@config` directive is allowed per file.");let s=n.params.match(/(['"])(.*?)\1/);if(!s)throw n.error("A path is required when using the `@config` directive.");let a=s[2];if(me.isAbsolute(a))throw n.error("The `@config` directive cannot be used with an absolute path.");if(t=me.resolve(me.dirname(i),a),!be.existsSync(t))throw n.error(`The config file at "${a}" does not exist. Make sure the path is correct and the file exists.`);n.remove()}),t||null}var Sy=P(()=>{u();ft();et()});var Ay=x((Wq,Ql)=>{u();Eg();xy();It();Sy();Ql.exports=function(e){return{postcssPlugin:"tailwindcss",plugins:[Ze.DEBUG&&function(t){return console.log(` -`),console.time("JIT TOTAL"),t},async function(t,i){e=ky(t,i)??e;let n=El(e);if(t.type==="document"){let s=t.nodes.filter(a=>a.type==="root");for(let a of s)a.type==="root"&&await Ts(n)(a,i);return}await Ts(n)(t,i)},Ze.DEBUG&&function(t){return console.timeEnd("JIT TOTAL"),console.log(` -`),t}].filter(Boolean)}};Ql.exports.postcss=!0});var _y=x((Gq,Cy)=>{u();Cy.exports=Ay()});var Yl=x((Qq,Ey)=>{u();Ey.exports=()=>["and_chr 114","and_uc 15.5","chrome 114","chrome 113","chrome 109","edge 114","firefox 114","ios_saf 16.5","ios_saf 16.4","ios_saf 16.3","ios_saf 16.1","opera 99","safari 16.5","samsung 21"]});var Rs={};Ge(Rs,{agents:()=>nO,feature:()=>sO});function sO(){return{status:"cr",title:"CSS Feature Queries",stats:{ie:{"6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","5.5":"n"},edge:{"12":"y","13":"y","14":"y","15":"y","16":"y","17":"y","18":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y"},firefox:{"2":"n","3":"n","4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y","3.5":"n","3.6":"n"},chrome:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"n","10":"n","11":"n","12":"n","13":"n","14":"n","15":"n","16":"n","17":"n","18":"n","19":"n","20":"n","21":"n","22":"n","23":"n","24":"n","25":"n","26":"n","27":"n","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","59":"y","60":"y","61":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","101":"y","102":"y","103":"y","104":"y","105":"y","106":"y","107":"y","108":"y","109":"y","110":"y","111":"y","112":"y","113":"y","114":"y","115":"y","116":"y","117":"y"},safari:{"4":"n","5":"n","6":"n","7":"n","8":"n","9":"y","10":"y","11":"y","12":"y","13":"y","14":"y","15":"y","17":"y","9.1":"y","10.1":"y","11.1":"y","12.1":"y","13.1":"y","14.1":"y","15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y",TP:"y","3.1":"n","3.2":"n","5.1":"n","6.1":"n","7.1":"n"},opera:{"9":"n","11":"n","12":"n","15":"y","16":"y","17":"y","18":"y","19":"y","20":"y","21":"y","22":"y","23":"y","24":"y","25":"y","26":"y","27":"y","28":"y","29":"y","30":"y","31":"y","32":"y","33":"y","34":"y","35":"y","36":"y","37":"y","38":"y","39":"y","40":"y","41":"y","42":"y","43":"y","44":"y","45":"y","46":"y","47":"y","48":"y","49":"y","50":"y","51":"y","52":"y","53":"y","54":"y","55":"y","56":"y","57":"y","58":"y","60":"y","62":"y","63":"y","64":"y","65":"y","66":"y","67":"y","68":"y","69":"y","70":"y","71":"y","72":"y","73":"y","74":"y","75":"y","76":"y","77":"y","78":"y","79":"y","80":"y","81":"y","82":"y","83":"y","84":"y","85":"y","86":"y","87":"y","88":"y","89":"y","90":"y","91":"y","92":"y","93":"y","94":"y","95":"y","96":"y","97":"y","98":"y","99":"y","100":"y","12.1":"y","9.5-9.6":"n","10.0-10.1":"n","10.5":"n","10.6":"n","11.1":"n","11.5":"n","11.6":"n"},ios_saf:{"8":"n","17":"y","9.0-9.2":"y","9.3":"y","10.0-10.2":"y","10.3":"y","11.0-11.2":"y","11.3-11.4":"y","12.0-12.1":"y","12.2-12.5":"y","13.0-13.1":"y","13.2":"y","13.3":"y","13.4-13.7":"y","14.0-14.4":"y","14.5-14.8":"y","15.0-15.1":"y","15.2-15.3":"y","15.4":"y","15.5":"y","15.6":"y","16.0":"y","16.1":"y","16.2":"y","16.3":"y","16.4":"y","16.5":"y","16.6":"y","3.2":"n","4.0-4.1":"n","4.2-4.3":"n","5.0-5.1":"n","6.0-6.1":"n","7.0-7.1":"n","8.1-8.4":"n"},op_mini:{all:"y"},android:{"3":"n","4":"n","114":"y","4.4":"y","4.4.3-4.4.4":"y","2.1":"n","2.2":"n","2.3":"n","4.1":"n","4.2-4.3":"n"},bb:{"7":"n","10":"n"},op_mob:{"10":"n","11":"n","12":"n","73":"y","11.1":"n","11.5":"n","12.1":"n"},and_chr:{"114":"y"},and_ff:{"115":"y"},ie_mob:{"10":"n","11":"n"},and_uc:{"15.5":"y"},samsung:{"4":"y","20":"y","21":"y","5.0-5.4":"y","6.2-6.4":"y","7.2-7.4":"y","8.2":"y","9.2":"y","10.1":"y","11.1-11.2":"y","12.0":"y","13.0":"y","14.0":"y","15.0":"y","16.0":"y","17.0":"y","18.0":"y","19.0":"y"},and_qq:{"13.1":"y"},baidu:{"13.18":"y"},kaios:{"2.5":"y","3.0-3.1":"y"}}}}var nO,Ps=P(()=>{u();nO={ie:{prefix:"ms"},edge:{prefix:"webkit",prefix_exceptions:{"12":"ms","13":"ms","14":"ms","15":"ms","16":"ms","17":"ms","18":"ms"}},firefox:{prefix:"moz"},chrome:{prefix:"webkit"},safari:{prefix:"webkit"},opera:{prefix:"webkit",prefix_exceptions:{"9":"o","11":"o","12":"o","9.5-9.6":"o","10.0-10.1":"o","10.5":"o","10.6":"o","11.1":"o","11.5":"o","11.6":"o","12.1":"o"}},ios_saf:{prefix:"webkit"},op_mini:{prefix:"o"},android:{prefix:"webkit"},bb:{prefix:"webkit"},op_mob:{prefix:"o",prefix_exceptions:{"73":"webkit"}},and_chr:{prefix:"webkit"},and_ff:{prefix:"moz"},ie_mob:{prefix:"ms"},and_uc:{prefix:"webkit",prefix_exceptions:{"15.5":"webkit"}},samsung:{prefix:"webkit"},and_qq:{prefix:"webkit"},baidu:{prefix:"webkit"},kaios:{prefix:"moz"}}});var Oy=x(()=>{u()});var _e=x((Xq,Lt)=>{u();var{list:Kl}=$e();Lt.exports.error=function(r){let e=new Error(r);throw e.autoprefixer=!0,e};Lt.exports.uniq=function(r){return[...new Set(r)]};Lt.exports.removeNote=function(r){return r.includes(" ")?r.split(" ")[0]:r};Lt.exports.escapeRegexp=function(r){return r.replace(/[$()*+-.?[\\\]^{|}]/g,"\\$&")};Lt.exports.regexp=function(r,e=!0){return e&&(r=this.escapeRegexp(r)),new RegExp(`(^|[\\s,(])(${r}($|[\\s(,]))`,"gi")};Lt.exports.editList=function(r,e){let t=Kl.comma(r),i=e(t,[]);if(t===i)return r;let n=r.match(/,\s*/);return n=n?n[0]:", ",i.join(n)};Lt.exports.splitSelector=function(r){return Kl.comma(r).map(e=>Kl.space(e).map(t=>t.split(/(?=\.|#)/g)))}});var Mt=x((Zq,Py)=>{u();var aO=Yl(),Ty=(Ps(),Rs).agents,oO=_e(),Ry=class{static prefixes(){if(this.prefixesCache)return this.prefixesCache;this.prefixesCache=[];for(let e in Ty)this.prefixesCache.push(`-${Ty[e].prefix}-`);return this.prefixesCache=oO.uniq(this.prefixesCache).sort((e,t)=>t.length-e.length),this.prefixesCache}static withPrefix(e){return this.prefixesRegexp||(this.prefixesRegexp=new RegExp(this.prefixes().join("|"))),this.prefixesRegexp.test(e)}constructor(e,t,i,n){this.data=e,this.options=i||{},this.browserslistOpts=n||{},this.selected=this.parse(t)}parse(e){let t={};for(let i in this.browserslistOpts)t[i]=this.browserslistOpts[i];return t.path=this.options.from,aO(e,t)}prefix(e){let[t,i]=e.split(" "),n=this.data[t],s=n.prefix_exceptions&&n.prefix_exceptions[i];return s||(s=n.prefix),`-${s}-`}isSelected(e){return this.selected.includes(e)}};Py.exports=Ry});var Ni=x((Jq,Iy)=>{u();Iy.exports={prefix(r){let e=r.match(/^(-\w+-)/);return e?e[0]:""},unprefixed(r){return r.replace(/^-\w+-/,"")}}});var wr=x((e$,qy)=>{u();var lO=Mt(),Dy=Ni(),uO=_e();function Xl(r,e){let t=new r.constructor;for(let i of Object.keys(r||{})){let n=r[i];i==="parent"&&typeof n=="object"?e&&(t[i]=e):i==="source"||i===null?t[i]=n:Array.isArray(n)?t[i]=n.map(s=>Xl(s,t)):i!=="_autoprefixerPrefix"&&i!=="_autoprefixerValues"&&i!=="proxyCache"&&(typeof n=="object"&&n!==null&&(n=Xl(n,t)),t[i]=n)}return t}var Is=class{static hack(e){return this.hacks||(this.hacks={}),e.names.map(t=>(this.hacks[t]=e,this.hacks[t]))}static load(e,t,i){let n=this.hacks&&this.hacks[e];return n?new n(e,t,i):new this(e,t,i)}static clone(e,t){let i=Xl(e);for(let n in t)i[n]=t[n];return i}constructor(e,t,i){this.prefixes=t,this.name=e,this.all=i}parentPrefix(e){let t;return typeof e._autoprefixerPrefix!="undefined"?t=e._autoprefixerPrefix:e.type==="decl"&&e.prop[0]==="-"?t=Dy.prefix(e.prop):e.type==="root"?t=!1:e.type==="rule"&&e.selector.includes(":-")&&/:(-\w+-)/.test(e.selector)?t=e.selector.match(/:(-\w+-)/)[1]:e.type==="atrule"&&e.name[0]==="-"?t=Dy.prefix(e.name):t=this.parentPrefix(e.parent),lO.prefixes().includes(t)||(t=!1),e._autoprefixerPrefix=t,e._autoprefixerPrefix}process(e,t){if(!this.check(e))return;let i=this.parentPrefix(e),n=this.prefixes.filter(a=>!i||i===uO.removeNote(a)),s=[];for(let a of n)this.add(e,a,s.concat([a]),t)&&s.push(a);return s}clone(e,t){return Is.clone(e,t)}};qy.exports=Is});var j=x((t$,My)=>{u();var fO=wr(),cO=Mt(),$y=_e(),Ly=class extends fO{check(){return!0}prefixed(e,t){return t+e}normalize(e){return e}otherPrefixes(e,t){for(let i of cO.prefixes())if(i!==t&&e.includes(i))return!0;return!1}set(e,t){return e.prop=this.prefixed(e.prop,t),e}needCascade(e){return e._autoprefixerCascade||(e._autoprefixerCascade=this.all.options.cascade!==!1&&e.raw("before").includes(` -`)),e._autoprefixerCascade}maxPrefixed(e,t){if(t._autoprefixerMax)return t._autoprefixerMax;let i=0;for(let n of e)n=$y.removeNote(n),n.length>i&&(i=n.length);return t._autoprefixerMax=i,t._autoprefixerMax}calcBefore(e,t,i=""){let s=this.maxPrefixed(e,t)-$y.removeNote(i).length,a=t.raw("before");return s>0&&(a+=Array(s).fill(" ").join("")),a}restoreBefore(e){let t=e.raw("before").split(` -`),i=t[t.length-1];this.all.group(e).up(n=>{let s=n.raw("before").split(` -`),a=s[s.length-1];a.lengtha.prop===n.prop&&a.value===n.value)))return this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,n)}isAlready(e,t){let i=this.all.group(e).up(n=>n.prop===t);return i||(i=this.all.group(e).down(n=>n.prop===t)),i}add(e,t,i,n){let s=this.prefixed(e.prop,t);if(!(this.isAlready(e,s)||this.otherPrefixes(e.value,t)))return this.insert(e,t,i,n)}process(e,t){if(!this.needCascade(e)){super.process(e,t);return}let i=super.process(e,t);!i||!i.length||(this.restoreBefore(e),e.raws.before=this.calcBefore(i,e))}old(e,t){return[this.prefixed(e,t)]}};My.exports=Ly});var By=x((r$,Ny)=>{u();Ny.exports=function r(e){return{mul:t=>new r(e*t),div:t=>new r(e/t),simplify:()=>new r(e),toString:()=>e.toString()}}});var zy=x((i$,jy)=>{u();var pO=By(),dO=wr(),Zl=_e(),hO=/(min|max)-resolution\s*:\s*\d*\.?\d+(dppx|dpcm|dpi|x)/gi,mO=/(min|max)-resolution(\s*:\s*)(\d*\.?\d+)(dppx|dpcm|dpi|x)/i,Fy=class extends dO{prefixName(e,t){return e==="-moz-"?t+"--moz-device-pixel-ratio":e+t+"-device-pixel-ratio"}prefixQuery(e,t,i,n,s){return n=new pO(n),s==="dpi"?n=n.div(96):s==="dpcm"&&(n=n.mul(2.54).div(96)),n=n.simplify(),e==="-o-"&&(n=n.n+"/"+n.d),this.prefixName(e,t)+i+n}clean(e){if(!this.bad){this.bad=[];for(let t of this.prefixes)this.bad.push(this.prefixName(t,"min")),this.bad.push(this.prefixName(t,"max"))}e.params=Zl.editList(e.params,t=>t.filter(i=>this.bad.every(n=>!i.includes(n))))}process(e){let t=this.parentPrefix(e),i=t?[t]:this.prefixes;e.params=Zl.editList(e.params,(n,s)=>{for(let a of n){if(!a.includes("min-resolution")&&!a.includes("max-resolution")){s.push(a);continue}for(let o of i){let l=a.replace(hO,c=>{let f=c.match(mO);return this.prefixQuery(o,f[1],f[2],f[3],f[4])});s.push(l)}s.push(a)}return Zl.uniq(s)})}};jy.exports=Fy});var Vy=x((n$,Uy)=>{u();var Jl="(".charCodeAt(0),eu=")".charCodeAt(0),Ds="'".charCodeAt(0),tu='"'.charCodeAt(0),ru="\\".charCodeAt(0),vr="/".charCodeAt(0),iu=",".charCodeAt(0),nu=":".charCodeAt(0),qs="*".charCodeAt(0),gO="u".charCodeAt(0),yO="U".charCodeAt(0),bO="+".charCodeAt(0),wO=/^[a-f0-9?-]+$/i;Uy.exports=function(r){for(var e=[],t=r,i,n,s,a,o,l,c,f,d=0,p=t.charCodeAt(d),h=t.length,b=[{nodes:e}],v=0,y,w="",k="",S="";d{u();Hy.exports=function r(e,t,i){var n,s,a,o;for(n=0,s=e.length;n{u();function Gy(r,e){var t=r.type,i=r.value,n,s;return e&&(s=e(r))!==void 0?s:t==="word"||t==="space"?i:t==="string"?(n=r.quote||"",n+i+(r.unclosed?"":n)):t==="comment"?"/*"+i+(r.unclosed?"":"*/"):t==="div"?(r.before||"")+i+(r.after||""):Array.isArray(r.nodes)?(n=Qy(r.nodes,e),t!=="function"?n:i+"("+(r.before||"")+n+(r.after||"")+(r.unclosed?"":")")):i}function Qy(r,e){var t,i;if(Array.isArray(r)){for(t="",i=r.length-1;~i;i-=1)t=Gy(r[i],e)+t;return t}return Gy(r,e)}Yy.exports=Qy});var Zy=x((o$,Xy)=>{u();var $s="-".charCodeAt(0),Ls="+".charCodeAt(0),su=".".charCodeAt(0),vO="e".charCodeAt(0),xO="E".charCodeAt(0);function kO(r){var e=r.charCodeAt(0),t;if(e===Ls||e===$s){if(t=r.charCodeAt(1),t>=48&&t<=57)return!0;var i=r.charCodeAt(2);return t===su&&i>=48&&i<=57}return e===su?(t=r.charCodeAt(1),t>=48&&t<=57):e>=48&&e<=57}Xy.exports=function(r){var e=0,t=r.length,i,n,s;if(t===0||!kO(r))return!1;for(i=r.charCodeAt(e),(i===Ls||i===$s)&&e++;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),i===su&&n>=48&&n<=57)for(e+=2;e57));)e+=1;if(i=r.charCodeAt(e),n=r.charCodeAt(e+1),s=r.charCodeAt(e+2),(i===vO||i===xO)&&(n>=48&&n<=57||(n===Ls||n===$s)&&s>=48&&s<=57))for(e+=n===Ls||n===$s?3:2;e57));)e+=1;return{number:r.slice(0,e),unit:r.slice(e)}}});var Ms=x((l$,tb)=>{u();var SO=Vy(),Jy=Wy(),eb=Ky();function Nt(r){return this instanceof Nt?(this.nodes=SO(r),this):new Nt(r)}Nt.prototype.toString=function(){return Array.isArray(this.nodes)?eb(this.nodes):""};Nt.prototype.walk=function(r,e){return Jy(this.nodes,r,e),this};Nt.unit=Zy();Nt.walk=Jy;Nt.stringify=eb;tb.exports=Nt});var ab=x((u$,sb)=>{u();var{list:AO}=$e(),rb=Ms(),CO=Mt(),ib=Ni(),nb=class{constructor(e){this.props=["transition","transition-property"],this.prefixes=e}add(e,t){let i,n,s=this.prefixes.add[e.prop],a=this.ruleVendorPrefixes(e),o=a||s&&s.prefixes||[],l=this.parse(e.value),c=l.map(h=>this.findProp(h)),f=[];if(c.some(h=>h[0]==="-"))return;for(let h of l){if(n=this.findProp(h),n[0]==="-")continue;let b=this.prefixes.add[n];if(!(!b||!b.prefixes))for(i of b.prefixes){if(a&&!a.some(y=>i.includes(y)))continue;let v=this.prefixes.prefixed(n,i);v!=="-ms-transform"&&!c.includes(v)&&(this.disabled(n,i)||f.push(this.clone(n,v,h)))}}l=l.concat(f);let d=this.stringify(l),p=this.stringify(this.cleanFromUnprefixed(l,"-webkit-"));if(o.includes("-webkit-")&&this.cloneBefore(e,`-webkit-${e.prop}`,p),this.cloneBefore(e,e.prop,p),o.includes("-o-")){let h=this.stringify(this.cleanFromUnprefixed(l,"-o-"));this.cloneBefore(e,`-o-${e.prop}`,h)}for(i of o)if(i!=="-webkit-"&&i!=="-o-"){let h=this.stringify(this.cleanOtherPrefixes(l,i));this.cloneBefore(e,i+e.prop,h)}d!==e.value&&!this.already(e,e.prop,d)&&(this.checkForWarning(t,e),e.cloneBefore(),e.value=d)}findProp(e){let t=e[0].value;if(/^\d/.test(t)){for(let[i,n]of e.entries())if(i!==0&&n.type==="word")return n.value}return t}already(e,t,i){return e.parent.some(n=>n.prop===t&&n.value===i)}cloneBefore(e,t,i){this.already(e,t,i)||e.cloneBefore({prop:t,value:i})}checkForWarning(e,t){if(t.prop!=="transition-property")return;let i=!1,n=!1;t.parent.each(s=>{if(s.type!=="decl"||s.prop.indexOf("transition-")!==0)return;let a=AO.comma(s.value);if(s.prop==="transition-property"){a.forEach(o=>{let l=this.prefixes.add[o];l&&l.prefixes&&l.prefixes.length>0&&(i=!0)});return}return n=n||a.length>1,!1}),i&&n&&t.warn(e,"Replace transition-property to transition, because Autoprefixer could not support any cases of transition-property and other transition-*")}remove(e){let t=this.parse(e.value);t=t.filter(a=>{let o=this.prefixes.remove[this.findProp(a)];return!o||!o.remove});let i=this.stringify(t);if(e.value===i)return;if(t.length===0){e.remove();return}let n=e.parent.some(a=>a.prop===e.prop&&a.value===i),s=e.parent.some(a=>a!==e&&a.prop===e.prop&&a.value.length>i.length);if(n||s){e.remove();return}e.value=i}parse(e){let t=rb(e),i=[],n=[];for(let s of t.nodes)n.push(s),s.type==="div"&&s.value===","&&(i.push(n),n=[]);return i.push(n),i.filter(s=>s.length>0)}stringify(e){if(e.length===0)return"";let t=[];for(let i of e)i[i.length-1].type!=="div"&&i.push(this.div(e)),t=t.concat(i);return t[0].type==="div"&&(t=t.slice(1)),t[t.length-1].type==="div"&&(t=t.slice(0,-2+1||void 0)),rb.stringify({nodes:t})}clone(e,t,i){let n=[],s=!1;for(let a of i)!s&&a.type==="word"&&a.value===e?(n.push({type:"word",value:t}),s=!0):n.push(a);return n}div(e){for(let t of e)for(let i of t)if(i.type==="div"&&i.value===",")return i;return{type:"div",value:",",after:" "}}cleanOtherPrefixes(e,t){return e.filter(i=>{let n=ib.prefix(this.findProp(i));return n===""||n===t})}cleanFromUnprefixed(e,t){let i=e.map(s=>this.findProp(s)).filter(s=>s.slice(0,t.length)===t).map(s=>this.prefixes.unprefixed(s)),n=[];for(let s of e){let a=this.findProp(s),o=ib.prefix(a);!i.includes(a)&&(o===t||o==="")&&n.push(s)}return n}disabled(e,t){let i=["order","justify-content","align-self","align-content"];if(e.includes("flex")||i.includes(e)){if(this.prefixes.options.flexbox===!1)return!0;if(this.prefixes.options.flexbox==="no-2009")return t.includes("2009")}}ruleVendorPrefixes(e){let{parent:t}=e;if(t.type!=="rule")return!1;if(!t.selector.includes(":-"))return!1;let i=CO.prefixes().filter(n=>t.selector.includes(":"+n));return i.length>0?i:!1}};sb.exports=nb});var xr=x((f$,lb)=>{u();var _O=_e(),ob=class{constructor(e,t,i,n){this.unprefixed=e,this.prefixed=t,this.string=i||t,this.regexp=n||_O.regexp(t)}check(e){return e.includes(this.string)?!!e.match(this.regexp):!1}};lb.exports=ob});var He=x((c$,fb)=>{u();var EO=wr(),OO=xr(),TO=Ni(),RO=_e(),ub=class extends EO{static save(e,t){let i=t.prop,n=[];for(let s in t._autoprefixerValues){let a=t._autoprefixerValues[s];if(a===t.value)continue;let o,l=TO.prefix(i);if(l==="-pie-")continue;if(l===s){o=t.value=a,n.push(o);continue}let c=e.prefixed(i,s),f=t.parent;if(!f.every(b=>b.prop!==c)){n.push(o);continue}let d=a.replace(/\s+/," ");if(f.some(b=>b.prop===t.prop&&b.value.replace(/\s+/," ")===d)){n.push(o);continue}let h=this.clone(t,{value:a});o=t.parent.insertBefore(t,h),n.push(o)}return n}check(e){let t=e.value;return t.includes(this.name)?!!t.match(this.regexp()):!1}regexp(){return this.regexpCache||(this.regexpCache=RO.regexp(this.name))}replace(e,t){return e.replace(this.regexp(),`$1${t}$2`)}value(e){return e.raws.value&&e.raws.value.value===e.value?e.raws.value.raw:e.value}add(e,t){e._autoprefixerValues||(e._autoprefixerValues={});let i=e._autoprefixerValues[t]||this.value(e),n;do if(n=i,i=this.replace(i,t),i===!1)return;while(i!==n);e._autoprefixerValues[t]=i}old(e){return new OO(this.name,e+this.name)}};fb.exports=ub});var Bt=x((p$,cb)=>{u();cb.exports={}});var ou=x((d$,hb)=>{u();var pb=Ms(),PO=He(),IO=Bt().insertAreas,DO=/(^|[^-])linear-gradient\(\s*(top|left|right|bottom)/i,qO=/(^|[^-])radial-gradient\(\s*\d+(\w*|%)\s+\d+(\w*|%)\s*,/i,$O=/(!\s*)?autoprefixer:\s*ignore\s+next/i,LO=/(!\s*)?autoprefixer\s*grid:\s*(on|off|(no-)?autoplace)/i,MO=["width","height","min-width","max-width","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size"];function au(r){return r.parent.some(e=>e.prop==="grid-template"||e.prop==="grid-template-areas")}function NO(r){let e=r.parent.some(i=>i.prop==="grid-template-rows"),t=r.parent.some(i=>i.prop==="grid-template-columns");return e&&t}var db=class{constructor(e){this.prefixes=e}add(e,t){let i=this.prefixes.add["@resolution"],n=this.prefixes.add["@keyframes"],s=this.prefixes.add["@viewport"],a=this.prefixes.add["@supports"];e.walkAtRules(f=>{if(f.name==="keyframes"){if(!this.disabled(f,t))return n&&n.process(f)}else if(f.name==="viewport"){if(!this.disabled(f,t))return s&&s.process(f)}else if(f.name==="supports"){if(this.prefixes.options.supports!==!1&&!this.disabled(f,t))return a.process(f)}else if(f.name==="media"&&f.params.includes("-resolution")&&!this.disabled(f,t))return i&&i.process(f)}),e.walkRules(f=>{if(!this.disabled(f,t))return this.prefixes.add.selectors.map(d=>d.process(f,t))});function o(f){return f.parent.nodes.some(d=>{if(d.type!=="decl")return!1;let p=d.prop==="display"&&/(inline-)?grid/.test(d.value),h=d.prop.startsWith("grid-template"),b=/^grid-([A-z]+-)?gap/.test(d.prop);return p||h||b})}function l(f){return f.parent.some(d=>d.prop==="display"&&/(inline-)?flex/.test(d.value))}let c=this.gridStatus(e,t)&&this.prefixes.add["grid-area"]&&this.prefixes.add["grid-area"].prefixes;return e.walkDecls(f=>{if(this.disabledDecl(f,t))return;let d=f.parent,p=f.prop,h=f.value;if(p==="grid-row-span"){t.warn("grid-row-span is not part of final Grid Layout. Use grid-row.",{node:f});return}else if(p==="grid-column-span"){t.warn("grid-column-span is not part of final Grid Layout. Use grid-column.",{node:f});return}else if(p==="display"&&h==="box"){t.warn("You should write display: flex by final spec instead of display: box",{node:f});return}else if(p==="text-emphasis-position")(h==="under"||h==="over")&&t.warn("You should use 2 values for text-emphasis-position For example, `under left` instead of just `under`.",{node:f});else if(/^(align|justify|place)-(items|content)$/.test(p)&&l(f))(h==="start"||h==="end")&&t.warn(`${h} value has mixed support, consider using flex-${h} instead`,{node:f});else if(p==="text-decoration-skip"&&h==="ink")t.warn("Replace text-decoration-skip: ink to text-decoration-skip-ink: auto, because spec had been changed",{node:f});else{if(c&&this.gridStatus(f,t))if(f.value==="subgrid"&&t.warn("IE does not support subgrid",{node:f}),/^(align|justify|place)-items$/.test(p)&&o(f)){let v=p.replace("-items","-self");t.warn(`IE does not support ${p} on grid containers. Try using ${v} on child elements instead: ${f.parent.selector} > * { ${v}: ${f.value} }`,{node:f})}else if(/^(align|justify|place)-content$/.test(p)&&o(f))t.warn(`IE does not support ${f.prop} on grid containers`,{node:f});else if(p==="display"&&f.value==="contents"){t.warn("Please do not use display: contents; if you have grid setting enabled",{node:f});return}else if(f.prop==="grid-gap"){let v=this.gridStatus(f,t);v==="autoplace"&&!NO(f)&&!au(f)?t.warn("grid-gap only works if grid-template(-areas) is being used or both rows and columns have been declared and cells have not been manually placed inside the explicit grid",{node:f}):(v===!0||v==="no-autoplace")&&!au(f)&&t.warn("grid-gap only works if grid-template(-areas) is being used",{node:f})}else if(p==="grid-auto-columns"){t.warn("grid-auto-columns is not supported by IE",{node:f});return}else if(p==="grid-auto-rows"){t.warn("grid-auto-rows is not supported by IE",{node:f});return}else if(p==="grid-auto-flow"){let v=d.some(w=>w.prop==="grid-template-rows"),y=d.some(w=>w.prop==="grid-template-columns");au(f)?t.warn("grid-auto-flow is not supported by IE",{node:f}):h.includes("dense")?t.warn("grid-auto-flow: dense is not supported by IE",{node:f}):!v&&!y&&t.warn("grid-auto-flow works only if grid-template-rows and grid-template-columns are present in the same rule",{node:f});return}else if(h.includes("auto-fit")){t.warn("auto-fit value is not supported by IE",{node:f,word:"auto-fit"});return}else if(h.includes("auto-fill")){t.warn("auto-fill value is not supported by IE",{node:f,word:"auto-fill"});return}else p.startsWith("grid-template")&&h.includes("[")&&t.warn("Autoprefixer currently does not support line names. Try using grid-template-areas instead.",{node:f,word:"["});if(h.includes("radial-gradient"))if(qO.test(f.value))t.warn("Gradient has outdated direction syntax. New syntax is like `closest-side at 0 0` instead of `0 0, closest-side`.",{node:f});else{let v=pb(h);for(let y of v.nodes)if(y.type==="function"&&y.value==="radial-gradient")for(let w of y.nodes)w.type==="word"&&(w.value==="cover"?t.warn("Gradient has outdated direction syntax. Replace `cover` to `farthest-corner`.",{node:f}):w.value==="contain"&&t.warn("Gradient has outdated direction syntax. Replace `contain` to `closest-side`.",{node:f}))}h.includes("linear-gradient")&&DO.test(h)&&t.warn("Gradient has outdated direction syntax. New syntax is like `to left` instead of `right`.",{node:f})}MO.includes(f.prop)&&(f.value.includes("-fill-available")||(f.value.includes("fill-available")?t.warn("Replace fill-available to stretch, because spec had been changed",{node:f}):f.value.includes("fill")&&pb(h).nodes.some(y=>y.type==="word"&&y.value==="fill")&&t.warn("Replace fill to stretch, because spec had been changed",{node:f})));let b;if(f.prop==="transition"||f.prop==="transition-property")return this.prefixes.transition.add(f,t);if(f.prop==="align-self"){if(this.displayType(f)!=="grid"&&this.prefixes.options.flexbox!==!1&&(b=this.prefixes.add["align-self"],b&&b.prefixes&&b.process(f)),this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-row-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="justify-self"){if(this.gridStatus(f,t)!==!1&&(b=this.prefixes.add["grid-column-align"],b&&b.prefixes))return b.process(f,t)}else if(f.prop==="place-self"){if(b=this.prefixes.add["place-self"],b&&b.prefixes&&this.gridStatus(f,t)!==!1)return b.process(f,t)}else if(b=this.prefixes.add[f.prop],b&&b.prefixes)return b.process(f,t)}),this.gridStatus(e,t)&&IO(e,this.disabled),e.walkDecls(f=>{if(this.disabledValue(f,t))return;let d=this.prefixes.unprefixed(f.prop),p=this.prefixes.values("add",d);if(Array.isArray(p))for(let h of p)h.process&&h.process(f,t);PO.save(this.prefixes,f)})}remove(e,t){let i=this.prefixes.remove["@resolution"];e.walkAtRules((n,s)=>{this.prefixes.remove[`@${n.name}`]?this.disabled(n,t)||n.parent.removeChild(s):n.name==="media"&&n.params.includes("-resolution")&&i&&i.clean(n)});for(let n of this.prefixes.remove.selectors)e.walkRules((s,a)=>{n.check(s)&&(this.disabled(s,t)||s.parent.removeChild(a))});return e.walkDecls((n,s)=>{if(this.disabled(n,t))return;let a=n.parent,o=this.prefixes.unprefixed(n.prop);if((n.prop==="transition"||n.prop==="transition-property")&&this.prefixes.transition.remove(n),this.prefixes.remove[n.prop]&&this.prefixes.remove[n.prop].remove){let l=this.prefixes.group(n).down(c=>this.prefixes.normalize(c.prop)===o);if(o==="flex-flow"&&(l=!0),n.prop==="-webkit-box-orient"){let c={"flex-direction":!0,"flex-flow":!0};if(!n.parent.some(f=>c[f.prop]))return}if(l&&!this.withHackValue(n)){n.raw("before").includes(` -`)&&this.reduceSpaces(n),a.removeChild(s);return}}for(let l of this.prefixes.values("remove",o)){if(!l.check||!l.check(n.value))continue;if(o=l.unprefixed,this.prefixes.group(n).down(f=>f.value.includes(o))){a.removeChild(s);return}}})}withHackValue(e){return e.prop==="-webkit-background-clip"&&e.value==="text"}disabledValue(e,t){return this.gridStatus(e,t)===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("grid")||this.prefixes.options.flexbox===!1&&e.type==="decl"&&e.prop==="display"&&e.value.includes("flex")||e.type==="decl"&&e.prop==="content"?!0:this.disabled(e,t)}disabledDecl(e,t){if(this.gridStatus(e,t)===!1&&e.type==="decl"&&(e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.prefixes.options.flexbox===!1&&e.type==="decl"){let i=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||i.includes(e.prop))return!0}return this.disabled(e,t)}disabled(e,t){if(!e)return!1;if(e._autoprefixerDisabled!==void 0)return e._autoprefixerDisabled;if(e.parent){let n=e.prev();if(n&&n.type==="comment"&&$O.test(n.text))return e._autoprefixerDisabled=!0,e._autoprefixerSelfDisabled=!0,!0}let i=null;if(e.nodes){let n;e.each(s=>{s.type==="comment"&&/(!\s*)?autoprefixer:\s*(off|on)/i.test(s.text)&&(typeof n!="undefined"?t.warn("Second Autoprefixer control comment was ignored. Autoprefixer applies control comment to whole block, not to next rules.",{node:s}):n=/on/i.test(s.text))}),n!==void 0&&(i=!n)}if(!e.nodes||i===null)if(e.parent){let n=this.disabled(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else i=!1;return e._autoprefixerDisabled=i,i}reduceSpaces(e){let t=!1;if(this.prefixes.group(e).up(()=>(t=!0,!0)),t)return;let i=e.raw("before").split(` -`),n=i[i.length-1].length,s=!1;this.prefixes.group(e).down(a=>{i=a.raw("before").split(` -`);let o=i.length-1;i[o].length>n&&(s===!1&&(s=i[o].length-n),i[o]=i[o].slice(0,-s),a.raws.before=i.join(` -`))})}displayType(e){for(let t of e.parent.nodes)if(t.prop==="display"){if(t.value.includes("flex"))return"flex";if(t.value.includes("grid"))return"grid"}return!1}gridStatus(e,t){if(!e)return!1;if(e._autoprefixerGridStatus!==void 0)return e._autoprefixerGridStatus;let i=null;if(e.nodes){let n;e.each(s=>{if(s.type==="comment"&&LO.test(s.text)){let a=/:\s*autoplace/i.test(s.text),o=/no-autoplace/i.test(s.text);typeof n!="undefined"?t.warn("Second Autoprefixer grid control comment was ignored. Autoprefixer applies control comments to the whole block, not to the next rules.",{node:s}):a?n="autoplace":o?n=!0:n=/on/i.test(s.text)}}),n!==void 0&&(i=n)}if(e.type==="atrule"&&e.name==="supports"){let n=e.params;n.includes("grid")&&n.includes("auto")&&(i=!1)}if(!e.nodes||i===null)if(e.parent){let n=this.gridStatus(e.parent,t);e.parent._autoprefixerSelfDisabled===!0?i=!1:i=n}else typeof this.prefixes.options.grid!="undefined"?i=this.prefixes.options.grid:typeof m.env.AUTOPREFIXER_GRID!="undefined"?m.env.AUTOPREFIXER_GRID==="autoplace"?i="autoplace":i=!0:i=!1;return e._autoprefixerGridStatus=i,i}};hb.exports=db});var gb=x((h$,mb)=>{u();mb.exports={A:{A:{"2":"K E F G A B JC"},B:{"1":"C L M H N D O P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I"},C:{"1":"2 3 4 5 6 7 8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 KC zB J K E F G A B C L M H N D O k l LC MC"},D:{"1":"8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB 0B dB 1B eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R S T U V W X Y Z a b c d e f g h i j n o p q r s t u v w x y z I uB 3B 4B","2":"0 1 2 3 4 5 6 7 J K E F G A B C L M H N D O k l"},E:{"1":"G A B C L M H D RC 6B vB wB 7B SC TC 8B 9B xB AC yB BC CC DC EC FC GC UC","2":"0 J K E F NC 5B OC PC QC"},F:{"1":"1 2 3 4 5 6 7 8 9 H N D O k l AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB TB UB VB WB XB YB ZB aB bB cB dB eB fB gB hB iB jB kB lB mB nB oB m pB qB rB sB tB P Q R 2B S T U V W X Y Z a b c d e f g h i j wB","2":"G B C VC WC XC YC vB HC ZC"},G:{"1":"D fC gC hC iC jC kC lC mC nC oC pC qC rC sC tC 8B 9B xB AC yB BC CC DC EC FC GC","2":"F 5B aC IC bC cC dC eC"},H:{"1":"uC"},I:{"1":"I zC 0C","2":"zB J vC wC xC yC IC"},J:{"2":"E A"},K:{"1":"m","2":"A B C vB HC wB"},L:{"1":"I"},M:{"1":"uB"},N:{"2":"A B"},O:{"1":"xB"},P:{"1":"J k l 1C 2C 3C 4C 5C 6B 6C 7C 8C 9C AD yB BD CD DD"},Q:{"1":"7B"},R:{"1":"ED"},S:{"1":"FD GD"}},B:4,C:"CSS Feature Queries"}});var vb=x((m$,wb)=>{u();function yb(r){return r[r.length-1]}var bb={parse(r){let e=[""],t=[e];for(let i of r){if(i==="("){e=[""],yb(t).push(e),t.push(e);continue}if(i===")"){t.pop(),e=yb(t),e.push("");continue}e[e.length-1]+=i}return t[0]},stringify(r){let e="";for(let t of r){if(typeof t=="object"){e+=`(${bb.stringify(t)})`;continue}e+=t}return e}};wb.exports=bb});var Cb=x((g$,Ab)=>{u();var BO=gb(),{feature:FO}=(Ps(),Rs),{parse:jO}=$e(),zO=Mt(),lu=vb(),UO=He(),VO=_e(),xb=FO(BO),kb=[];for(let r in xb.stats){let e=xb.stats[r];for(let t in e){let i=e[t];/y/.test(i)&&kb.push(r+" "+t)}}var Sb=class{constructor(e,t){this.Prefixes=e,this.all=t}prefixer(){if(this.prefixerCache)return this.prefixerCache;let e=this.all.browsers.selected.filter(i=>kb.includes(i)),t=new zO(this.all.browsers.data,e,this.all.options);return this.prefixerCache=new this.Prefixes(this.all.data,t,this.all.options),this.prefixerCache}parse(e){let t=e.split(":"),i=t[0],n=t[1];return n||(n=""),[i.trim(),n.trim()]}virtual(e){let[t,i]=this.parse(e),n=jO("a{}").first;return n.append({prop:t,value:i,raws:{before:""}}),n}prefixed(e){let t=this.virtual(e);if(this.disabled(t.first))return t.nodes;let i={warn:()=>null},n=this.prefixer().add[t.first.prop];n&&n.process&&n.process(t.first,i);for(let s of t.nodes){for(let a of this.prefixer().values("add",t.first.prop))a.process(s);UO.save(this.all,s)}return t.nodes}isNot(e){return typeof e=="string"&&/not\s*/i.test(e)}isOr(e){return typeof e=="string"&&/\s*or\s*/i.test(e)}isProp(e){return typeof e=="object"&&e.length===1&&typeof e[0]=="string"}isHack(e,t){return!new RegExp(`(\\(|\\s)${VO.escapeRegexp(t)}:`).test(e)}toRemove(e,t){let[i,n]=this.parse(e),s=this.all.unprefixed(i),a=this.all.cleaner();if(a.remove[i]&&a.remove[i].remove&&!this.isHack(t,s))return!0;for(let o of a.values("remove",s))if(o.check(n))return!0;return!1}remove(e,t){let i=0;for(;itypeof t!="object"?t:t.length===1&&typeof t[0]=="object"?this.cleanBrackets(t[0]):this.cleanBrackets(t))}convert(e){let t=[""];for(let i of e)t.push([`${i.prop}: ${i.value}`]),t.push(" or ");return t[t.length-1]="",t}normalize(e){if(typeof e!="object")return e;if(e=e.filter(t=>t!==""),typeof e[0]=="string"){let t=e[0].trim();if(t.includes(":")||t==="selector"||t==="not selector")return[lu.stringify(e)]}return e.map(t=>this.normalize(t))}add(e,t){return e.map(i=>{if(this.isProp(i)){let n=this.prefixed(i[0]);return n.length>1?this.convert(n):i}return typeof i=="object"?this.add(i,t):i})}process(e){let t=lu.parse(e.params);t=this.normalize(t),t=this.remove(t,e.params),t=this.add(t,e.params),t=this.cleanBrackets(t),e.params=lu.stringify(t)}disabled(e){if(!this.all.options.grid&&(e.prop==="display"&&e.value.includes("grid")||e.prop.includes("grid")||e.prop==="justify-items"))return!0;if(this.all.options.flexbox===!1){if(e.prop==="display"&&e.value.includes("flex"))return!0;let t=["order","justify-content","align-items","align-content"];if(e.prop.includes("flex")||t.includes(e.prop))return!0}return!1}};Ab.exports=Sb});var Ob=x((y$,Eb)=>{u();var _b=class{constructor(e,t){this.prefix=t,this.prefixed=e.prefixed(this.prefix),this.regexp=e.regexp(this.prefix),this.prefixeds=e.possible().map(i=>[e.prefixed(i),e.regexp(i)]),this.unprefixed=e.name,this.nameRegexp=e.regexp()}isHack(e){let t=e.parent.index(e)+1,i=e.parent.nodes;for(;t{u();var{list:HO}=$e(),WO=Ob(),GO=wr(),QO=Mt(),YO=_e(),Tb=class extends GO{constructor(e,t,i){super(e,t,i);this.regexpCache=new Map}check(e){return e.selector.includes(this.name)?!!e.selector.match(this.regexp()):!1}prefixed(e){return this.name.replace(/^(\W*)/,`$1${e}`)}regexp(e){if(!this.regexpCache.has(e)){let t=e?this.prefixed(e):this.name;this.regexpCache.set(e,new RegExp(`(^|[^:"'=])${YO.escapeRegexp(t)}`,"gi"))}return this.regexpCache.get(e)}possible(){return QO.prefixes()}prefixeds(e){if(e._autoprefixerPrefixeds){if(e._autoprefixerPrefixeds[this.name])return e._autoprefixerPrefixeds}else e._autoprefixerPrefixeds={};let t={};if(e.selector.includes(",")){let n=HO.comma(e.selector).filter(s=>s.includes(this.name));for(let s of this.possible())t[s]=n.map(a=>this.replace(a,s)).join(", ")}else for(let i of this.possible())t[i]=this.replace(e.selector,i);return e._autoprefixerPrefixeds[this.name]=t,e._autoprefixerPrefixeds}already(e,t,i){let n=e.parent.index(e)-1;for(;n>=0;){let s=e.parent.nodes[n];if(s.type!=="rule")return!1;let a=!1;for(let o in t[this.name]){let l=t[this.name][o];if(s.selector===l){if(i===o)return!0;a=!0;break}}if(!a)return!1;n-=1}return!1}replace(e,t){return e.replace(this.regexp(),`$1${this.prefixed(t)}`)}add(e,t){let i=this.prefixeds(e);if(this.already(e,i,t))return;let n=this.clone(e,{selector:i[this.name][t]});e.parent.insertBefore(e,n)}old(e){return new WO(this,e)}};Rb.exports=Tb});var Db=x((w$,Ib)=>{u();var KO=wr(),Pb=class extends KO{add(e,t){let i=t+e.name;if(e.parent.some(a=>a.name===i&&a.params===e.params))return;let s=this.clone(e,{name:i});return e.parent.insertBefore(e,s)}process(e){let t=this.parentPrefix(e);for(let i of this.prefixes)(!t||t===i)&&this.add(e,i)}};Ib.exports=Pb});var $b=x((v$,qb)=>{u();var XO=kr(),uu=class extends XO{prefixed(e){return e==="-webkit-"?":-webkit-full-screen":e==="-moz-"?":-moz-full-screen":`:${e}fullscreen`}};uu.names=[":fullscreen"];qb.exports=uu});var Mb=x((x$,Lb)=>{u();var ZO=kr(),fu=class extends ZO{possible(){return super.possible().concat(["-moz- old","-ms- old"])}prefixed(e){return e==="-webkit-"?"::-webkit-input-placeholder":e==="-ms-"?"::-ms-input-placeholder":e==="-ms- old"?":-ms-input-placeholder":e==="-moz- old"?":-moz-placeholder":`::${e}placeholder`}};fu.names=["::placeholder"];Lb.exports=fu});var Bb=x((k$,Nb)=>{u();var JO=kr(),cu=class extends JO{prefixed(e){return e==="-ms-"?":-ms-input-placeholder":`:${e}placeholder-shown`}};cu.names=[":placeholder-shown"];Nb.exports=cu});var jb=x((S$,Fb)=>{u();var eT=kr(),tT=_e(),pu=class extends eT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=tT.uniq(this.prefixes.map(n=>"-webkit-")))}prefixed(e){return e==="-webkit-"?"::-webkit-file-upload-button":`::${e}file-selector-button`}};pu.names=["::file-selector-button"];Fb.exports=pu});var Pe=x((A$,zb)=>{u();zb.exports=function(r){let e;return r==="-webkit- 2009"||r==="-moz-"?e=2009:r==="-ms-"?e=2012:r==="-webkit-"&&(e="final"),r==="-webkit- 2009"&&(r="-webkit-"),[e,r]}});var Wb=x((C$,Hb)=>{u();var Ub=$e().list,Vb=Pe(),rT=j(),Sr=class extends rT{prefixed(e,t){let i;return[i,t]=Vb(t),i===2009?t+"box-flex":super.prefixed(e,t)}normalize(){return"flex"}set(e,t){let i=Vb(t)[0];if(i===2009)return e.value=Ub.space(e.value)[0],e.value=Sr.oldValues[e.value]||e.value,super.set(e,t);if(i===2012){let n=Ub.space(e.value);n.length===3&&n[2]==="0"&&(e.value=n.slice(0,2).concat("0px").join(" "))}return super.set(e,t)}};Sr.names=["flex","box-flex"];Sr.oldValues={auto:"1",none:"0"};Hb.exports=Sr});var Yb=x((_$,Qb)=>{u();var Gb=Pe(),iT=j(),du=class extends iT{prefixed(e,t){let i;return[i,t]=Gb(t),i===2009?t+"box-ordinal-group":i===2012?t+"flex-order":super.prefixed(e,t)}normalize(){return"order"}set(e,t){return Gb(t)[0]===2009&&/\d/.test(e.value)?(e.value=(parseInt(e.value)+1).toString(),super.set(e,t)):super.set(e,t)}};du.names=["order","flex-order","box-ordinal-group"];Qb.exports=du});var Xb=x((E$,Kb)=>{u();var nT=j(),hu=class extends nT{check(e){let t=e.value;return!t.toLowerCase().includes("alpha(")&&!t.includes("DXImageTransform.Microsoft")&&!t.includes("data:image/svg+xml")}};hu.names=["filter"];Kb.exports=hu});var Jb=x((O$,Zb)=>{u();var sT=j(),mu=class extends sT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=this.clone(e),a=e.prop.replace(/end$/,"start"),o=t+e.prop.replace(/end$/,"span");if(!e.parent.some(l=>l.prop===o)){if(s.prop=o,e.value.includes("span"))s.value=e.value.replace(/span\s/i,"");else{let l;if(e.parent.walkDecls(a,c=>{l=c}),l){let c=Number(e.value)-Number(l.value)+"";s.value=c}else e.warn(n,`Can not prefix ${e.prop} (${a} is not found)`)}e.cloneBefore(s)}}};mu.names=["grid-row-end","grid-column-end"];Zb.exports=mu});var tw=x((T$,ew)=>{u();var aT=j(),gu=class extends aT{check(e){return!e.value.split(/\s+/).some(t=>{let i=t.toLowerCase();return i==="reverse"||i==="alternate-reverse"})}};gu.names=["animation","animation-direction"];ew.exports=gu});var iw=x((R$,rw)=>{u();var oT=Pe(),lT=j(),yu=class extends lT{insert(e,t,i){let n;if([n,t]=oT(t),n!==2009)return super.insert(e,t,i);let s=e.value.split(/\s+/).filter(d=>d!=="wrap"&&d!=="nowrap"&&"wrap-reverse");if(s.length===0||e.parent.some(d=>d.prop===t+"box-orient"||d.prop===t+"box-direction"))return;let o=s[0],l=o.includes("row")?"horizontal":"vertical",c=o.includes("reverse")?"reverse":"normal",f=this.clone(e);return f.prop=t+"box-orient",f.value=l,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f),f=this.clone(e),f.prop=t+"box-direction",f.value=c,this.needCascade(e)&&(f.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,f)}};yu.names=["flex-flow","box-direction","box-orient"];rw.exports=yu});var sw=x((P$,nw)=>{u();var uT=Pe(),fT=j(),bu=class extends fT{normalize(){return"flex"}prefixed(e,t){let i;return[i,t]=uT(t),i===2009?t+"box-flex":i===2012?t+"flex-positive":super.prefixed(e,t)}};bu.names=["flex-grow","flex-positive"];nw.exports=bu});var ow=x((I$,aw)=>{u();var cT=Pe(),pT=j(),wu=class extends pT{set(e,t){if(cT(t)[0]!==2009)return super.set(e,t)}};wu.names=["flex-wrap"];aw.exports=wu});var uw=x((D$,lw)=>{u();var dT=j(),Ar=Bt(),vu=class extends dT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=Ar.parse(e),[a,o]=Ar.translate(s,0,2),[l,c]=Ar.translate(s,1,3);[["grid-row",a],["grid-row-span",o],["grid-column",l],["grid-column-span",c]].forEach(([f,d])=>{Ar.insertDecl(e,f,d)}),Ar.warnTemplateSelectorNotFound(e,n),Ar.warnIfGridRowColumnExists(e,n)}};vu.names=["grid-area"];lw.exports=vu});var cw=x((q$,fw)=>{u();var hT=j(),Bi=Bt(),xu=class extends hT{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(a=>a.prop==="-ms-grid-row-align"))return;let[[n,s]]=Bi.parse(e);s?(Bi.insertDecl(e,"grid-row-align",n),Bi.insertDecl(e,"grid-column-align",s)):(Bi.insertDecl(e,"grid-row-align",n),Bi.insertDecl(e,"grid-column-align",n))}};xu.names=["place-self"];fw.exports=xu});var dw=x(($$,pw)=>{u();var mT=j(),ku=class extends mT{check(e){let t=e.value;return!t.includes("/")||t.includes("span")}normalize(e){return e.replace("-start","")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-ms-"&&(i=i.replace("-start","")),i}};ku.names=["grid-row-start","grid-column-start"];pw.exports=ku});var gw=x((L$,mw)=>{u();var hw=Pe(),gT=j(),Cr=class extends gT{check(e){return e.parent&&!e.parent.some(t=>t.prop&&t.prop.startsWith("grid-"))}prefixed(e,t){let i;return[i,t]=hw(t),i===2012?t+"flex-item-align":super.prefixed(e,t)}normalize(){return"align-self"}set(e,t){let i=hw(t)[0];if(i===2012)return e.value=Cr.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Cr.names=["align-self","flex-item-align"];Cr.oldValues={"flex-end":"end","flex-start":"start"};mw.exports=Cr});var bw=x((M$,yw)=>{u();var yT=j(),bT=_e(),Su=class extends yT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=bT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Su.names=["appearance"];yw.exports=Su});var xw=x((N$,vw)=>{u();var ww=Pe(),wT=j(),Au=class extends wT{normalize(){return"flex-basis"}prefixed(e,t){let i;return[i,t]=ww(t),i===2012?t+"flex-preferred-size":super.prefixed(e,t)}set(e,t){let i;if([i,t]=ww(t),i===2012||i==="final")return super.set(e,t)}};Au.names=["flex-basis","flex-preferred-size"];vw.exports=Au});var Sw=x((B$,kw)=>{u();var vT=j(),Cu=class extends vT{normalize(){return this.name.replace("box-image","border")}prefixed(e,t){let i=super.prefixed(e,t);return t==="-webkit-"&&(i=i.replace("border","box-image")),i}};Cu.names=["mask-border","mask-border-source","mask-border-slice","mask-border-width","mask-border-outset","mask-border-repeat","mask-box-image","mask-box-image-source","mask-box-image-slice","mask-box-image-width","mask-box-image-outset","mask-box-image-repeat"];kw.exports=Cu});var Cw=x((F$,Aw)=>{u();var xT=j(),lt=class extends xT{insert(e,t,i){let n=e.prop==="mask-composite",s;n?s=e.value.split(","):s=e.value.match(lt.regexp)||[],s=s.map(c=>c.trim()).filter(c=>c);let a=s.length,o;if(a&&(o=this.clone(e),o.value=s.map(c=>lt.oldValues[c]||c).join(", "),s.includes("intersect")&&(o.value+=", xor"),o.prop=t+"mask-composite"),n)return a?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):void 0;let l=this.clone(e);return l.prop=t+l.prop,a&&(l.value=l.value.replace(lt.regexp,"")),this.needCascade(e)&&(l.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,l),a?(this.needCascade(e)&&(o.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,o)):e}};lt.names=["mask","mask-composite"];lt.oldValues={add:"source-over",subtract:"source-out",intersect:"source-in",exclude:"xor"};lt.regexp=new RegExp(`\\s+(${Object.keys(lt.oldValues).join("|")})\\b(?!\\))\\s*(?=[,])`,"ig");Aw.exports=lt});var Ow=x((j$,Ew)=>{u();var _w=Pe(),kT=j(),_r=class extends kT{prefixed(e,t){let i;return[i,t]=_w(t),i===2009?t+"box-align":i===2012?t+"flex-align":super.prefixed(e,t)}normalize(){return"align-items"}set(e,t){let i=_w(t)[0];return(i===2009||i===2012)&&(e.value=_r.oldValues[e.value]||e.value),super.set(e,t)}};_r.names=["align-items","flex-align","box-align"];_r.oldValues={"flex-end":"end","flex-start":"start"};Ew.exports=_r});var Rw=x((z$,Tw)=>{u();var ST=j(),_u=class extends ST{set(e,t){return t==="-ms-"&&e.value==="contain"&&(e.value="element"),super.set(e,t)}insert(e,t,i){if(!(e.value==="all"&&t==="-ms-"))return super.insert(e,t,i)}};_u.names=["user-select"];Tw.exports=_u});var Dw=x((U$,Iw)=>{u();var Pw=Pe(),AT=j(),Eu=class extends AT{normalize(){return"flex-shrink"}prefixed(e,t){let i;return[i,t]=Pw(t),i===2012?t+"flex-negative":super.prefixed(e,t)}set(e,t){let i;if([i,t]=Pw(t),i===2012||i==="final")return super.set(e,t)}};Eu.names=["flex-shrink","flex-negative"];Iw.exports=Eu});var $w=x((V$,qw)=>{u();var CT=j(),Ou=class extends CT{prefixed(e,t){return`${t}column-${e}`}normalize(e){return e.includes("inside")?"break-inside":e.includes("before")?"break-before":"break-after"}set(e,t){return(e.prop==="break-inside"&&e.value==="avoid-column"||e.value==="avoid-page")&&(e.value="avoid"),super.set(e,t)}insert(e,t,i){if(e.prop!=="break-inside")return super.insert(e,t,i);if(!(/region/i.test(e.value)||/page/i.test(e.value)))return super.insert(e,t,i)}};Ou.names=["break-inside","page-break-inside","column-break-inside","break-before","page-break-before","column-break-before","break-after","page-break-after","column-break-after"];qw.exports=Ou});var Mw=x((H$,Lw)=>{u();var _T=j(),Tu=class extends _T{prefixed(e,t){return t+"print-color-adjust"}normalize(){return"color-adjust"}};Tu.names=["color-adjust","print-color-adjust"];Lw.exports=Tu});var Bw=x((W$,Nw)=>{u();var ET=j(),Er=class extends ET{insert(e,t,i){if(t==="-ms-"){let n=this.set(this.clone(e),t);this.needCascade(e)&&(n.raws.before=this.calcBefore(i,e,t));let s="ltr";return e.parent.nodes.forEach(a=>{a.prop==="direction"&&(a.value==="rtl"||a.value==="ltr")&&(s=a.value)}),n.value=Er.msValues[s][e.value]||e.value,e.parent.insertBefore(e,n)}return super.insert(e,t,i)}};Er.names=["writing-mode"];Er.msValues={ltr:{"horizontal-tb":"lr-tb","vertical-rl":"tb-rl","vertical-lr":"tb-lr"},rtl:{"horizontal-tb":"rl-tb","vertical-rl":"bt-rl","vertical-lr":"bt-lr"}};Nw.exports=Er});var jw=x((G$,Fw)=>{u();var OT=j(),Ru=class extends OT{set(e,t){return e.value=e.value.replace(/\s+fill(\s)/,"$1"),super.set(e,t)}};Ru.names=["border-image"];Fw.exports=Ru});var Vw=x((Q$,Uw)=>{u();var zw=Pe(),TT=j(),Or=class extends TT{prefixed(e,t){let i;return[i,t]=zw(t),i===2012?t+"flex-line-pack":super.prefixed(e,t)}normalize(){return"align-content"}set(e,t){let i=zw(t)[0];if(i===2012)return e.value=Or.oldValues[e.value]||e.value,super.set(e,t);if(i==="final")return super.set(e,t)}};Or.names=["align-content","flex-line-pack"];Or.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};Uw.exports=Or});var Ww=x((Y$,Hw)=>{u();var RT=j(),We=class extends RT{prefixed(e,t){return t==="-moz-"?t+(We.toMozilla[e]||e):super.prefixed(e,t)}normalize(e){return We.toNormal[e]||e}};We.names=["border-radius"];We.toMozilla={};We.toNormal={};for(let r of["top","bottom"])for(let e of["left","right"]){let t=`border-${r}-${e}-radius`,i=`border-radius-${r}${e}`;We.names.push(t),We.names.push(i),We.toMozilla[t]=i,We.toNormal[i]=t}Hw.exports=We});var Qw=x((K$,Gw)=>{u();var PT=j(),Pu=class extends PT{prefixed(e,t){return e.includes("-start")?t+e.replace("-block-start","-before"):t+e.replace("-block-end","-after")}normalize(e){return e.includes("-before")?e.replace("-before","-block-start"):e.replace("-after","-block-end")}};Pu.names=["border-block-start","border-block-end","margin-block-start","margin-block-end","padding-block-start","padding-block-end","border-before","border-after","margin-before","margin-after","padding-before","padding-after"];Gw.exports=Pu});var Kw=x((X$,Yw)=>{u();var IT=j(),{parseTemplate:DT,warnMissedAreas:qT,getGridGap:$T,warnGridGap:LT,inheritGridGap:MT}=Bt(),Iu=class extends IT{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);if(e.parent.some(h=>h.prop==="-ms-grid-rows"))return;let s=$T(e),a=MT(e,s),{rows:o,columns:l,areas:c}=DT({decl:e,gap:a||s}),f=Object.keys(c).length>0,d=Boolean(o),p=Boolean(l);return LT({gap:s,hasColumns:p,decl:e,result:n}),qT(c,e,n),(d&&p||f)&&e.cloneBefore({prop:"-ms-grid-rows",value:o,raws:{}}),p&&e.cloneBefore({prop:"-ms-grid-columns",value:l,raws:{}}),e}};Iu.names=["grid-template"];Yw.exports=Iu});var Zw=x((Z$,Xw)=>{u();var NT=j(),Du=class extends NT{prefixed(e,t){return t+e.replace("-inline","")}normalize(e){return e.replace(/(margin|padding|border)-(start|end)/,"$1-inline-$2")}};Du.names=["border-inline-start","border-inline-end","margin-inline-start","margin-inline-end","padding-inline-start","padding-inline-end","border-start","border-end","margin-start","margin-end","padding-start","padding-end"];Xw.exports=Du});var e0=x((J$,Jw)=>{u();var BT=j(),qu=class extends BT{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-row-align"}normalize(){return"align-self"}};qu.names=["grid-row-align"];Jw.exports=qu});var r0=x((eL,t0)=>{u();var FT=j(),Tr=class extends FT{keyframeParents(e){let{parent:t}=e;for(;t;){if(t.type==="atrule"&&t.name==="keyframes")return!0;({parent:t}=t)}return!1}contain3d(e){if(e.prop==="transform-origin")return!1;for(let t of Tr.functions3d)if(e.value.includes(`${t}(`))return!0;return!1}set(e,t){return e=super.set(e,t),t==="-ms-"&&(e.value=e.value.replace(/rotatez/gi,"rotate")),e}insert(e,t,i){if(t==="-ms-"){if(!this.contain3d(e)&&!this.keyframeParents(e))return super.insert(e,t,i)}else if(t==="-o-"){if(!this.contain3d(e))return super.insert(e,t,i)}else return super.insert(e,t,i)}};Tr.names=["transform","transform-origin"];Tr.functions3d=["matrix3d","translate3d","translateZ","scale3d","scaleZ","rotate3d","rotateX","rotateY","perspective"];t0.exports=Tr});var s0=x((tL,n0)=>{u();var i0=Pe(),jT=j(),$u=class extends jT{normalize(){return"flex-direction"}insert(e,t,i){let n;if([n,t]=i0(t),n!==2009)return super.insert(e,t,i);if(e.parent.some(f=>f.prop===t+"box-orient"||f.prop===t+"box-direction"))return;let a=e.value,o,l;a==="inherit"||a==="initial"||a==="unset"?(o=a,l=a):(o=a.includes("row")?"horizontal":"vertical",l=a.includes("reverse")?"reverse":"normal");let c=this.clone(e);return c.prop=t+"box-orient",c.value=o,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c),c=this.clone(e),c.prop=t+"box-direction",c.value=l,this.needCascade(e)&&(c.raws.before=this.calcBefore(i,e,t)),e.parent.insertBefore(e,c)}old(e,t){let i;return[i,t]=i0(t),i===2009?[t+"box-orient",t+"box-direction"]:super.old(e,t)}};$u.names=["flex-direction","box-direction","box-orient"];n0.exports=$u});var o0=x((rL,a0)=>{u();var zT=j(),Lu=class extends zT{check(e){return e.value==="pixelated"}prefixed(e,t){return t==="-ms-"?"-ms-interpolation-mode":super.prefixed(e,t)}set(e,t){return t!=="-ms-"?super.set(e,t):(e.prop="-ms-interpolation-mode",e.value="nearest-neighbor",e)}normalize(){return"image-rendering"}process(e,t){return super.process(e,t)}};Lu.names=["image-rendering","interpolation-mode"];a0.exports=Lu});var u0=x((iL,l0)=>{u();var UT=j(),VT=_e(),Mu=class extends UT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=VT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}};Mu.names=["backdrop-filter"];l0.exports=Mu});var c0=x((nL,f0)=>{u();var HT=j(),WT=_e(),Nu=class extends HT{constructor(e,t,i){super(e,t,i);this.prefixes&&(this.prefixes=WT.uniq(this.prefixes.map(n=>n==="-ms-"?"-webkit-":n)))}check(e){return e.value.toLowerCase()==="text"}};Nu.names=["background-clip"];f0.exports=Nu});var d0=x((sL,p0)=>{u();var GT=j(),QT=["none","underline","overline","line-through","blink","inherit","initial","unset"],Bu=class extends GT{check(e){return e.value.split(/\s+/).some(t=>!QT.includes(t))}};Bu.names=["text-decoration"];p0.exports=Bu});var g0=x((aL,m0)=>{u();var h0=Pe(),YT=j(),Rr=class extends YT{prefixed(e,t){let i;return[i,t]=h0(t),i===2009?t+"box-pack":i===2012?t+"flex-pack":super.prefixed(e,t)}normalize(){return"justify-content"}set(e,t){let i=h0(t)[0];if(i===2009||i===2012){let n=Rr.oldValues[e.value]||e.value;if(e.value=n,i!==2009||n!=="distribute")return super.set(e,t)}else if(i==="final")return super.set(e,t)}};Rr.names=["justify-content","flex-pack","box-pack"];Rr.oldValues={"flex-end":"end","flex-start":"start","space-between":"justify","space-around":"distribute"};m0.exports=Rr});var b0=x((oL,y0)=>{u();var KT=j(),Fu=class extends KT{set(e,t){let i=e.value.toLowerCase();return t==="-webkit-"&&!i.includes(" ")&&i!=="contain"&&i!=="cover"&&(e.value=e.value+" "+e.value),super.set(e,t)}};Fu.names=["background-size"];y0.exports=Fu});var v0=x((lL,w0)=>{u();var XT=j(),ju=Bt(),zu=class extends XT{insert(e,t,i){if(t!=="-ms-")return super.insert(e,t,i);let n=ju.parse(e),[s,a]=ju.translate(n,0,1);n[0]&&n[0].includes("span")&&(a=n[0].join("").replace(/\D/g,"")),[[e.prop,s],[`${e.prop}-span`,a]].forEach(([l,c])=>{ju.insertDecl(e,l,c)})}};zu.names=["grid-row","grid-column"];w0.exports=zu});var S0=x((uL,k0)=>{u();var ZT=j(),{prefixTrackProp:x0,prefixTrackValue:JT,autoplaceGridItems:eR,getGridGap:tR,inheritGridGap:rR}=Bt(),iR=ou(),Uu=class extends ZT{prefixed(e,t){return t==="-ms-"?x0({prop:e,prefix:t}):super.prefixed(e,t)}normalize(e){return e.replace(/^grid-(rows|columns)/,"grid-template-$1")}insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let{parent:s,prop:a,value:o}=e,l=a.includes("rows"),c=a.includes("columns"),f=s.some(k=>k.prop==="grid-template"||k.prop==="grid-template-areas");if(f&&l)return!1;let d=new iR({options:{}}),p=d.gridStatus(s,n),h=tR(e);h=rR(e,h)||h;let b=l?h.row:h.column;(p==="no-autoplace"||p===!0)&&!f&&(b=null);let v=JT({value:o,gap:b});e.cloneBefore({prop:x0({prop:a,prefix:t}),value:v});let y=s.nodes.find(k=>k.prop==="grid-auto-flow"),w="row";if(y&&!d.disabled(y,n)&&(w=y.value.trim()),p==="autoplace"){let k=s.nodes.find(E=>E.prop==="grid-template-rows");if(!k&&f)return;if(!k&&!f){e.warn(n,"Autoplacement does not work without grid-template-rows property");return}!s.nodes.find(E=>E.prop==="grid-template-columns")&&!f&&e.warn(n,"Autoplacement does not work without grid-template-columns property"),c&&!f&&eR(e,n,h,w)}}};Uu.names=["grid-template-rows","grid-template-columns","grid-rows","grid-columns"];k0.exports=Uu});var C0=x((fL,A0)=>{u();var nR=j(),Vu=class extends nR{check(e){return!e.value.includes("flex-")&&e.value!=="baseline"}prefixed(e,t){return t+"grid-column-align"}normalize(){return"justify-self"}};Vu.names=["grid-column-align"];A0.exports=Vu});var E0=x((cL,_0)=>{u();var sR=j(),Hu=class extends sR{prefixed(e,t){return t+"scroll-chaining"}normalize(){return"overscroll-behavior"}set(e,t){return e.value==="auto"?e.value="chained":(e.value==="none"||e.value==="contain")&&(e.value="none"),super.set(e,t)}};Hu.names=["overscroll-behavior","scroll-chaining"];_0.exports=Hu});var R0=x((pL,T0)=>{u();var aR=j(),{parseGridAreas:oR,warnMissedAreas:lR,prefixTrackProp:uR,prefixTrackValue:O0,getGridGap:fR,warnGridGap:cR,inheritGridGap:pR}=Bt();function dR(r){return r.trim().slice(1,-1).split(/["']\s*["']?/g)}var Wu=class extends aR{insert(e,t,i,n){if(t!=="-ms-")return super.insert(e,t,i);let s=!1,a=!1,o=e.parent,l=fR(e);l=pR(e,l)||l,o.walkDecls(/-ms-grid-rows/,d=>d.remove()),o.walkDecls(/grid-template-(rows|columns)/,d=>{if(d.prop==="grid-template-rows"){a=!0;let{prop:p,value:h}=d;d.cloneBefore({prop:uR({prop:p,prefix:t}),value:O0({value:h,gap:l.row})})}else s=!0});let c=dR(e.value);s&&!a&&l.row&&c.length>1&&e.cloneBefore({prop:"-ms-grid-rows",value:O0({value:`repeat(${c.length}, auto)`,gap:l.row}),raws:{}}),cR({gap:l,hasColumns:s,decl:e,result:n});let f=oR({rows:c,gap:l});return lR(f,e,n),e}};Wu.names=["grid-template-areas"];T0.exports=Wu});var I0=x((dL,P0)=>{u();var hR=j(),Gu=class extends hR{set(e,t){return t==="-webkit-"&&(e.value=e.value.replace(/\s*(right|left)\s*/i,"")),super.set(e,t)}};Gu.names=["text-emphasis-position"];P0.exports=Gu});var q0=x((hL,D0)=>{u();var mR=j(),Qu=class extends mR{set(e,t){return e.prop==="text-decoration-skip-ink"&&e.value==="auto"?(e.prop=t+"text-decoration-skip",e.value="ink",e):super.set(e,t)}};Qu.names=["text-decoration-skip-ink","text-decoration-skip"];D0.exports=Qu});var F0=x((mL,B0)=>{u();"use strict";B0.exports={wrap:$0,limit:L0,validate:M0,test:Yu,curry:gR,name:N0};function $0(r,e,t){var i=e-r;return((t-r)%i+i)%i+r}function L0(r,e,t){return Math.max(r,Math.min(e,t))}function M0(r,e,t,i,n){if(!Yu(r,e,t,i,n))throw new Error(t+" is outside of range ["+r+","+e+")");return t}function Yu(r,e,t,i,n){return!(te||n&&t===e||i&&t===r)}function N0(r,e,t,i){return(t?"(":"[")+r+","+e+(i?")":"]")}function gR(r,e,t,i){var n=N0.bind(null,r,e,t,i);return{wrap:$0.bind(null,r,e),limit:L0.bind(null,r,e),validate:function(s){return M0(r,e,s,t,i)},test:function(s){return Yu(r,e,s,t,i)},toString:n,name:n}}});var U0=x((gL,z0)=>{u();var Ku=Ms(),yR=F0(),bR=xr(),wR=He(),vR=_e(),j0=/top|left|right|bottom/gi,wt=class extends wR{replace(e,t){let i=Ku(e);for(let n of i.nodes)if(n.type==="function"&&n.value===this.name)if(n.nodes=this.newDirection(n.nodes),n.nodes=this.normalize(n.nodes),t==="-webkit- old"){if(!this.oldWebkit(n))return!1}else n.nodes=this.convertDirection(n.nodes),n.value=t+n.value;return i.toString()}replaceFirst(e,...t){return t.map(n=>n===" "?{type:"space",value:n}:{type:"word",value:n}).concat(e.slice(1))}normalizeUnit(e,t){return`${parseFloat(e)/t*360}deg`}normalize(e){if(!e[0])return e;if(/-?\d+(.\d+)?grad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,400);else if(/-?\d+(.\d+)?rad/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,2*Math.PI);else if(/-?\d+(.\d+)?turn/.test(e[0].value))e[0].value=this.normalizeUnit(e[0].value,1);else if(e[0].value.includes("deg")){let t=parseFloat(e[0].value);t=yR.wrap(0,360,t),e[0].value=`${t}deg`}return e[0].value==="0deg"?e=this.replaceFirst(e,"to"," ","top"):e[0].value==="90deg"?e=this.replaceFirst(e,"to"," ","right"):e[0].value==="180deg"?e=this.replaceFirst(e,"to"," ","bottom"):e[0].value==="270deg"&&(e=this.replaceFirst(e,"to"," ","left")),e}newDirection(e){if(e[0].value==="to"||(j0.lastIndex=0,!j0.test(e[0].value)))return e;e.unshift({type:"word",value:"to"},{type:"space",value:" "});for(let t=2;t0&&(e[0].value==="to"?this.fixDirection(e):e[0].value.includes("deg")?this.fixAngle(e):this.isRadial(e)&&this.fixRadial(e)),e}fixDirection(e){e.splice(0,2);for(let t of e){if(t.type==="div")break;t.type==="word"&&(t.value=this.revertDirection(t.value))}}fixAngle(e){let t=e[0].value;t=parseFloat(t),t=Math.abs(450-t)%360,t=this.roundFloat(t,3),e[0].value=`${t}deg`}fixRadial(e){let t=[],i=[],n,s,a,o,l;for(o=0;o{u();var xR=xr(),kR=He();function V0(r){return new RegExp(`(^|[\\s,(])(${r}($|[\\s),]))`,"gi")}var Xu=class extends kR{regexp(){return this.regexpCache||(this.regexpCache=V0(this.name)),this.regexpCache}isStretch(){return this.name==="stretch"||this.name==="fill"||this.name==="fill-available"}replace(e,t){return t==="-moz-"&&this.isStretch()?e.replace(this.regexp(),"$1-moz-available$3"):t==="-webkit-"&&this.isStretch()?e.replace(this.regexp(),"$1-webkit-fill-available$3"):super.replace(e,t)}old(e){let t=e+this.name;return this.isStretch()&&(e==="-moz-"?t="-moz-available":e==="-webkit-"&&(t="-webkit-fill-available")),new xR(this.name,t,t,V0(t))}add(e,t){if(!(e.prop.includes("grid")&&t!=="-webkit-"))return super.add(e,t)}};Xu.names=["max-content","min-content","fit-content","fill","fill-available","stretch"];H0.exports=Xu});var Y0=x((bL,Q0)=>{u();var G0=xr(),SR=He(),Zu=class extends SR{replace(e,t){return t==="-webkit-"?e.replace(this.regexp(),"$1-webkit-optimize-contrast"):t==="-moz-"?e.replace(this.regexp(),"$1-moz-crisp-edges"):super.replace(e,t)}old(e){return e==="-webkit-"?new G0(this.name,"-webkit-optimize-contrast"):e==="-moz-"?new G0(this.name,"-moz-crisp-edges"):super.old(e)}};Zu.names=["pixelated"];Q0.exports=Zu});var X0=x((wL,K0)=>{u();var AR=He(),Ju=class extends AR{replace(e,t){let i=super.replace(e,t);return t==="-webkit-"&&(i=i.replace(/("[^"]+"|'[^']+')(\s+\d+\w)/gi,"url($1)$2")),i}};Ju.names=["image-set"];K0.exports=Ju});var J0=x((vL,Z0)=>{u();var CR=$e().list,_R=He(),ef=class extends _R{replace(e,t){return CR.space(e).map(i=>{if(i.slice(0,+this.name.length+1)!==this.name+"(")return i;let n=i.lastIndexOf(")"),s=i.slice(n+1),a=i.slice(this.name.length+1,n);if(t==="-webkit-"){let o=a.match(/\d*.?\d+%?/);o?(a=a.slice(o[0].length).trim(),a+=`, ${o[0]}`):a+=", 0.5"}return t+this.name+"("+a+")"+s}).join(" ")}};ef.names=["cross-fade"];Z0.exports=ef});var tv=x((xL,ev)=>{u();var ER=Pe(),OR=xr(),TR=He(),tf=class extends TR{constructor(e,t){super(e,t);e==="display-flex"&&(this.name="flex")}check(e){return e.prop==="display"&&e.value===this.name}prefixed(e){let t,i;return[t,e]=ER(e),t===2009?this.name==="flex"?i="box":i="inline-box":t===2012?this.name==="flex"?i="flexbox":i="inline-flexbox":t==="final"&&(i=this.name),e+i}replace(e,t){return this.prefixed(t)}old(e){let t=this.prefixed(e);if(!!t)return new OR(this.name,t)}};tf.names=["display-flex","inline-flex"];ev.exports=tf});var iv=x((kL,rv)=>{u();var RR=He(),rf=class extends RR{constructor(e,t){super(e,t);e==="display-grid"&&(this.name="grid")}check(e){return e.prop==="display"&&e.value===this.name}};rf.names=["display-grid","inline-grid"];rv.exports=rf});var sv=x((SL,nv)=>{u();var PR=He(),nf=class extends PR{constructor(e,t){super(e,t);e==="filter-function"&&(this.name="filter")}};nf.names=["filter","filter-function"];nv.exports=nf});var uv=x((AL,lv)=>{u();var av=Ni(),z=j(),ov=zy(),IR=ab(),DR=ou(),qR=Cb(),sf=Mt(),Pr=kr(),$R=Db(),ut=He(),Ir=_e(),LR=$b(),MR=Mb(),NR=Bb(),BR=jb(),FR=Wb(),jR=Yb(),zR=Xb(),UR=Jb(),VR=tw(),HR=iw(),WR=sw(),GR=ow(),QR=uw(),YR=cw(),KR=dw(),XR=gw(),ZR=bw(),JR=xw(),e5=Sw(),t5=Cw(),r5=Ow(),i5=Rw(),n5=Dw(),s5=$w(),a5=Mw(),o5=Bw(),l5=jw(),u5=Vw(),f5=Ww(),c5=Qw(),p5=Kw(),d5=Zw(),h5=e0(),m5=r0(),g5=s0(),y5=o0(),b5=u0(),w5=c0(),v5=d0(),x5=g0(),k5=b0(),S5=v0(),A5=S0(),C5=C0(),_5=E0(),E5=R0(),O5=I0(),T5=q0(),R5=U0(),P5=W0(),I5=Y0(),D5=X0(),q5=J0(),$5=tv(),L5=iv(),M5=sv();Pr.hack(LR);Pr.hack(MR);Pr.hack(NR);Pr.hack(BR);z.hack(FR);z.hack(jR);z.hack(zR);z.hack(UR);z.hack(VR);z.hack(HR);z.hack(WR);z.hack(GR);z.hack(QR);z.hack(YR);z.hack(KR);z.hack(XR);z.hack(ZR);z.hack(JR);z.hack(e5);z.hack(t5);z.hack(r5);z.hack(i5);z.hack(n5);z.hack(s5);z.hack(a5);z.hack(o5);z.hack(l5);z.hack(u5);z.hack(f5);z.hack(c5);z.hack(p5);z.hack(d5);z.hack(h5);z.hack(m5);z.hack(g5);z.hack(y5);z.hack(b5);z.hack(w5);z.hack(v5);z.hack(x5);z.hack(k5);z.hack(S5);z.hack(A5);z.hack(C5);z.hack(_5);z.hack(E5);z.hack(O5);z.hack(T5);ut.hack(R5);ut.hack(P5);ut.hack(I5);ut.hack(D5);ut.hack(q5);ut.hack($5);ut.hack(L5);ut.hack(M5);var af=new Map,Fi=class{constructor(e,t,i={}){this.data=e,this.browsers=t,this.options=i,[this.add,this.remove]=this.preprocess(this.select(this.data)),this.transition=new IR(this),this.processor=new DR(this)}cleaner(){if(this.cleanerCache)return this.cleanerCache;if(this.browsers.selected.length){let e=new sf(this.browsers.data,[]);this.cleanerCache=new Fi(this.data,e,this.options)}else return this;return this.cleanerCache}select(e){let t={add:{},remove:{}};for(let i in e){let n=e[i],s=n.browsers.map(l=>{let c=l.split(" ");return{browser:`${c[0]} ${c[1]}`,note:c[2]}}),a=s.filter(l=>l.note).map(l=>`${this.browsers.prefix(l.browser)} ${l.note}`);a=Ir.uniq(a),s=s.filter(l=>this.browsers.isSelected(l.browser)).map(l=>{let c=this.browsers.prefix(l.browser);return l.note?`${c} ${l.note}`:c}),s=this.sort(Ir.uniq(s)),this.options.flexbox==="no-2009"&&(s=s.filter(l=>!l.includes("2009")));let o=n.browsers.map(l=>this.browsers.prefix(l));n.mistakes&&(o=o.concat(n.mistakes)),o=o.concat(a),o=Ir.uniq(o),s.length?(t.add[i]=s,s.length!s.includes(l)))):t.remove[i]=o}return t}sort(e){return e.sort((t,i)=>{let n=Ir.removeNote(t).length,s=Ir.removeNote(i).length;return n===s?i.length-t.length:s-n})}preprocess(e){let t={selectors:[],"@supports":new qR(Fi,this)};for(let n in e.add){let s=e.add[n];if(n==="@keyframes"||n==="@viewport")t[n]=new $R(n,s,this);else if(n==="@resolution")t[n]=new ov(n,s,this);else if(this.data[n].selector)t.selectors.push(Pr.load(n,s,this));else{let a=this.data[n].props;if(a){let o=ut.load(n,s,this);for(let l of a)t[l]||(t[l]={values:[]}),t[l].values.push(o)}else{let o=t[n]&&t[n].values||[];t[n]=z.load(n,s,this),t[n].values=o}}}let i={selectors:[]};for(let n in e.remove){let s=e.remove[n];if(this.data[n].selector){let a=Pr.load(n,s);for(let o of s)i.selectors.push(a.old(o))}else if(n==="@keyframes"||n==="@viewport")for(let a of s){let o=`@${a}${n.slice(1)}`;i[o]={remove:!0}}else if(n==="@resolution")i[n]=new ov(n,s,this);else{let a=this.data[n].props;if(a){let o=ut.load(n,[],this);for(let l of s){let c=o.old(l);if(c)for(let f of a)i[f]||(i[f]={}),i[f].values||(i[f].values=[]),i[f].values.push(c)}}else for(let o of s){let l=this.decl(n).old(n,o);if(n==="align-self"){let c=t[n]&&t[n].prefixes;if(c){if(o==="-webkit- 2009"&&c.includes("-webkit-"))continue;if(o==="-webkit-"&&c.includes("-webkit- 2009"))continue}}for(let c of l)i[c]||(i[c]={}),i[c].remove=!0}}}return[t,i]}decl(e){return af.has(e)||af.set(e,z.load(e)),af.get(e)}unprefixed(e){let t=this.normalize(av.unprefixed(e));return t==="flex-direction"&&(t="flex-flow"),t}normalize(e){return this.decl(e).normalize(e)}prefixed(e,t){return e=av.unprefixed(e),this.decl(e).prefixed(e,t)}values(e,t){let i=this[e],n=i["*"]&&i["*"].values,s=i[t]&&i[t].values;return n&&s?Ir.uniq(n.concat(s)):n||s||[]}group(e){let t=e.parent,i=t.index(e),{length:n}=t.nodes,s=this.unprefixed(e.prop),a=(o,l)=>{for(i+=o;i>=0&&i{u();fv.exports={"backdrop-filter":{feature:"css-backdrop-filter",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},element:{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-element-function",browsers:["firefox 114"]},"user-select":{mistakes:["-khtml-"],feature:"user-select-none",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"background-clip":{feature:"background-clip-text",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},hyphens:{feature:"css-hyphens",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},fill:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"fill-available":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},stretch:{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"fit-content":{props:["width","min-width","max-width","height","min-height","max-height","inline-size","min-inline-size","max-inline-size","block-size","min-block-size","max-block-size","grid","grid-template","grid-template-rows","grid-template-columns","grid-auto-columns","grid-auto-rows"],feature:"intrinsic-width",browsers:["firefox 114"]},"text-decoration-style":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-color":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-line":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-decoration-skip-ink":{feature:"text-decoration",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"text-size-adjust":{feature:"text-size-adjust",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5"]},"mask-clip":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-composite":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-image":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-origin":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-repeat":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-source":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},mask:{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-position":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-size":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-outset":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-width":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"mask-border-slice":{feature:"css-masks",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},"clip-path":{feature:"css-clip-path",browsers:["samsung 21"]},"box-decoration-break":{feature:"css-boxdecorationbreak",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","opera 99","safari 16.5","samsung 21"]},appearance:{feature:"css-appearance",browsers:["samsung 21"]},"image-set":{props:["background","background-image","border-image","cursor","mask","mask-image","list-style","list-style-image","content"],feature:"css-image-set",browsers:["and_uc 15.5","chrome 109","samsung 21"]},"cross-fade":{props:["background","background-image","border-image","mask","list-style","list-style-image","content","mask-image"],feature:"css-cross-fade",browsers:["and_chr 114","and_uc 15.5","chrome 109","chrome 113","chrome 114","edge 114","opera 99","samsung 21"]},isolate:{props:["unicode-bidi"],feature:"css-unicode-bidi",browsers:["ios_saf 16.1","ios_saf 16.3","ios_saf 16.4","ios_saf 16.5","safari 16.5"]},"color-adjust":{feature:"css-color-adjust",browsers:["chrome 109","chrome 113","chrome 114","edge 114","opera 99"]}}});var dv=x((_L,pv)=>{u();pv.exports={}});var yv=x((EL,gv)=>{u();var N5=Yl(),{agents:B5}=(Ps(),Rs),of=Oy(),F5=Mt(),j5=uv(),z5=cv(),U5=dv(),hv={browsers:B5,prefixes:z5},mv=` - Replace Autoprefixer \`browsers\` option to Browserslist config. - Use \`browserslist\` key in \`package.json\` or \`.browserslistrc\` file. - - Using \`browsers\` option can cause errors. Browserslist config can - be used for Babel, Autoprefixer, postcss-normalize and other tools. - - If you really need to use option, rename it to \`overrideBrowserslist\`. - - Learn more at: - https://github.com/browserslist/browserslist#readme - https://twitter.com/browserslist - -`;function V5(r){return Object.prototype.toString.apply(r)==="[object Object]"}var lf=new Map;function H5(r,e){e.browsers.selected.length!==0&&(e.add.selectors.length>0||Object.keys(e.add).length>2||r.warn(`Autoprefixer target browsers do not need any prefixes.You do not need Autoprefixer anymore. -Check your Browserslist config to be sure that your targets are set up correctly. - - Learn more at: - https://github.com/postcss/autoprefixer#readme - https://github.com/browserslist/browserslist#readme - -`))}gv.exports=Dr;function Dr(...r){let e;if(r.length===1&&V5(r[0])?(e=r[0],r=void 0):r.length===0||r.length===1&&!r[0]?r=void 0:r.length<=2&&(Array.isArray(r[0])||!r[0])?(e=r[1],r=r[0]):typeof r[r.length-1]=="object"&&(e=r.pop()),e||(e={}),e.browser)throw new Error("Change `browser` option to `overrideBrowserslist` in Autoprefixer");if(e.browserslist)throw new Error("Change `browserslist` option to `overrideBrowserslist` in Autoprefixer");e.overrideBrowserslist?r=e.overrideBrowserslist:e.browsers&&(typeof console!="undefined"&&console.warn&&(of.red?console.warn(of.red(mv.replace(/`[^`]+`/g,n=>of.yellow(n.slice(1,-1))))):console.warn(mv)),r=e.browsers);let t={ignoreUnknownVersions:e.ignoreUnknownVersions,stats:e.stats,env:e.env};function i(n){let s=hv,a=new F5(s.browsers,r,n,t),o=a.selected.join(", ")+JSON.stringify(e);return lf.has(o)||lf.set(o,new j5(s.prefixes,a,e)),lf.get(o)}return{postcssPlugin:"autoprefixer",prepare(n){let s=i({from:n.opts.from,env:e.env});return{OnceExit(a){H5(n,s),e.remove!==!1&&s.processor.remove(a,n),e.add!==!1&&s.processor.add(a,n)}}},info(n){return n=n||{},n.from=n.from||m.cwd(),U5(i(n))},options:e,browsers:r}}Dr.postcss=!0;Dr.data=hv;Dr.defaults=N5.defaults;Dr.info=()=>Dr().info()});var bv={};Ge(bv,{default:()=>W5});var W5,wv=P(()=>{u();W5=[]});var xv={};Ge(xv,{default:()=>G5});var vv,G5,kv=P(()=>{u();Xi();vv=pe(rn()),G5=St(vv.default.theme)});var Av={};Ge(Av,{default:()=>Q5});var Sv,Q5,Cv=P(()=>{u();Xi();Sv=pe(rn()),Q5=St(Sv.default)});u();"use strict";var Y5=vt(_y()),K5=vt($e()),X5=vt(yv()),Z5=vt((wv(),bv)),J5=vt((kv(),xv)),eP=vt((Cv(),Av)),tP=vt((Vs(),_f)),rP=vt((al(),sl)),iP=vt((sa(),sc));function vt(r){return r&&r.__esModule?r:{default:r}}console.warn("cdn.tailwindcss.com should not be used in production. To use Tailwind CSS in production, install it as a PostCSS plugin or use the Tailwind CLI: https://tailwindcss.com/docs/installation");var Ns="tailwind",uf="text/tailwindcss",_v="/template.html",Yt,Ev=!0,Ov=0,ff=new Set,cf,Tv="",Rv=(r=!1)=>({get(e,t){return(!r||t==="config")&&typeof e[t]=="object"&&e[t]!==null?new Proxy(e[t],Rv()):e[t]},set(e,t,i){return e[t]=i,(!r||t==="config")&&pf(!0),!0}});window[Ns]=new Proxy({config:{},defaultTheme:J5.default,defaultConfig:eP.default,colors:tP.default,plugin:rP.default,resolveConfig:iP.default},Rv(!0));function Pv(r){cf.observe(r,{attributes:!0,attributeFilter:["type"],characterData:!0,subtree:!0,childList:!0})}new MutationObserver(async r=>{let e=!1;if(!cf){cf=new MutationObserver(async()=>await pf(!0));for(let t of document.querySelectorAll(`style[type="${uf}"]`))Pv(t)}for(let t of r)for(let i of t.addedNodes)i.nodeType===1&&i.tagName==="STYLE"&&i.getAttribute("type")===uf&&(Pv(i),e=!0);await pf(e)}).observe(document.documentElement,{attributes:!0,attributeFilter:["class"],childList:!0,subtree:!0});async function pf(r=!1){r&&(Ov++,ff.clear());let e="";for(let i of document.querySelectorAll(`style[type="${uf}"]`))e+=i.textContent;let t=new Set;for(let i of document.querySelectorAll("[class]"))for(let n of i.classList)ff.has(n)||t.add(n);if(document.body&&(Ev||t.size>0||e!==Tv||!Yt||!Yt.isConnected)){for(let n of t)ff.add(n);Ev=!1,Tv=e,self[_v]=Array.from(t).join(" ");let{css:i}=await(0,K5.default)([(0,Y5.default)({...window[Ns].config,_hash:Ov,content:{files:[_v],extract:{html:n=>n.split(" ")}},plugins:[...Z5.default,...Array.isArray(window[Ns].config.plugins)?window[Ns].config.plugins:[]]}),(0,X5.default)({remove:!1})]).process(`@tailwind base;@tailwind components;@tailwind utilities;${e}`);(!Yt||!Yt.isConnected)&&(Yt=document.createElement("style"),document.head.append(Yt)),Yt.textContent=i}}})(); -/*! - * fill-range - * - * Copyright (c) 2014-present, Jon Schlinkert. - * Licensed under the MIT License. - */ -/*! - * is-number - * - * Copyright (c) 2014-present, Jon Schlinkert. - * Released under the MIT License. - */ -/*! - * to-regex-range - * - * Copyright (c) 2015-present, Jon Schlinkert. - * Released under the MIT License. - */ -/*! https://mths.be/cssesc v3.0.0 by @mathias */ From 0b1275dc04c5629a83816223e050037589d6a8b4 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 23 Mar 2026 12:45:52 +0200 Subject: [PATCH 16/48] fix(ci): move tailwind download rule before targets that depend on it The TAILWIND_JS variable and download rule were defined after the e2e and ci-test targets. GNU Make 3.81 doesn't resolve forward-referenced file target prerequisites, causing the curl download to be skipped. --- Makefile | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index f64021ccf..7bcad10b9 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,12 @@ GOLANGCI_LINT_VERSION ?= 2.11.4 TAILWIND_VERSION ?= 3.4.17 TAILWIND_JS = auth/oidc/static/tailwind.min.js +$(TAILWIND_JS): + curl -sL "https://cdn.tailwindcss.com/$(TAILWIND_VERSION)" -o $(TAILWIND_JS) + +TAILWIND_VERSION ?= 3.4.17 +TAILWIND_JS = auth/oidc/static/tailwind.min.js + $(TAILWIND_JS): curl -sL "https://cdn.tailwindcss.com/$(TAILWIND_VERSION)" -o $(TAILWIND_JS) @@ -143,15 +149,6 @@ manifests: generate gen-schemas ## Generate WebhookConfiguration, ClusterRole an generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object paths="./api/..." paths="./logs/..." -TAILWIND_VERSION ?= 3.4.17 -TAILWIND_JS = auth/oidc/static/tailwind.min.js - -$(TAILWIND_JS): - curl -sL "https://cdn.tailwindcss.com/$(TAILWIND_VERSION)" -o $(TAILWIND_JS) - -.PHONY: static -static: $(TAILWIND_JS) - .PHONY: build build: static go build -o ./.bin/$(NAME) -ldflags "-X \"main.version=$(VERSION_TAG) built at $(DATE)\"" main.go From e1806b6ddcf9fa6e8a1e0427eedb44fac3099690 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 23 Mar 2026 14:17:11 +0200 Subject: [PATCH 17/48] fix(ci): build binary before ci-test for OIDC e2e tests --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 7bcad10b9..f6832c997 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,7 @@ test: .PHONY: ci-test ci-test: $(TAILWIND_JS) + go build -o ./.bin/$(NAME) main.go ginkgo -r --skip-package=tests/e2e --keep-going --junit-report junit-report.xml --github-output --output-dir test-reports --succinct .PHONY: e2e From 4a84886a44c4455bc67306a678c136b9ed7b82d1 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 23 Mar 2026 14:48:52 +0200 Subject: [PATCH 18/48] refactor: move oidc_e2e tests to tests/e2e/oidc, add chrome no-sandbox --- Makefile | 1 + go.sum | 24 +++-- tests/e2e/oidc/oidc_login_test.go | 6 +- tests/e2e/oidc/suite_test.go | 2 +- tests/oidc_e2e/oidc_login_test.go | 142 ------------------------------ tests/oidc_e2e/suite_test.go | 110 ----------------------- 6 files changed, 22 insertions(+), 263 deletions(-) delete mode 100644 tests/oidc_e2e/oidc_login_test.go delete mode 100644 tests/oidc_e2e/suite_test.go diff --git a/Makefile b/Makefile index f6832c997..61c5b5992 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,7 @@ ci-test: $(TAILWIND_JS) .PHONY: e2e e2e: $(TAILWIND_JS) + go build -o ./.bin/$(NAME) main.go ginkgo -r --keep-going ./tests/e2e/... fmt: diff --git a/go.sum b/go.sum index ad0f8c72e..8f5c7b0be 100644 --- a/go.sum +++ b/go.sum @@ -287,9 +287,6 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= @@ -397,7 +394,6 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -458,6 +454,7 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01 github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -553,6 +550,7 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= @@ -889,7 +887,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= @@ -927,7 +924,6 @@ github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3P github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= @@ -1224,6 +1220,22 @@ github.com/zitadel/oidc/v3 v3.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrD github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= +gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow= +gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= +gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M= +gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw= +gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/tests/e2e/oidc/oidc_login_test.go b/tests/e2e/oidc/oidc_login_test.go index 8d0f906bc..dd7be6e83 100644 --- a/tests/e2e/oidc/oidc_login_test.go +++ b/tests/e2e/oidc/oidc_login_test.go @@ -22,10 +22,8 @@ var _ = ginkgo.Describe("OIDC Browser Login Flow", ginkgo.Label("slow"), ginkgo. verifier, challenge, err := oidcclient.GeneratePKCE() Expect(err).ToNot(HaveOccurred()) - state, err := oidcclient.RandomBase64(16) - Expect(err).ToNot(HaveOccurred()) - nonce, err := oidcclient.RandomBase64(16) - Expect(err).ToNot(HaveOccurred()) + state := oidcclient.RandomBase64(16) + nonce := oidcclient.RandomBase64(16) endpoints, err = oidcclient.Discover(serverURL + "/.well-known/openid-configuration") Expect(err).ToNot(HaveOccurred()) diff --git a/tests/e2e/oidc/suite_test.go b/tests/e2e/oidc/suite_test.go index e123c3494..0b0c78fcb 100644 --- a/tests/e2e/oidc/suite_test.go +++ b/tests/e2e/oidc/suite_test.go @@ -72,7 +72,7 @@ var _ = ginkgo.BeforeSuite(func() { "--auth", "basic", "--htpasswd-file", htpasswdPath, "--oidc", - "--frontend-url", serverURL, + "--public-endpoint", serverURL, "--httpPort", strconv.Itoa(serverPort), "--disable-postgrest", "--postgrest-uri", "", diff --git a/tests/oidc_e2e/oidc_login_test.go b/tests/oidc_e2e/oidc_login_test.go deleted file mode 100644 index f1c605a12..000000000 --- a/tests/oidc_e2e/oidc_login_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package oidc_e2e - -import ( - "context" - "fmt" - "net" - "net/http" - "net/url" - "time" - - "github.com/chromedp/chromedp" - "github.com/flanksource/incident-commander/auth/oidcclient" - ginkgo "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = ginkgo.Describe("OIDC Browser Login Flow", ginkgo.Label("slow"), ginkgo.Ordered, func() { - var tokens *oidcclient.Tokens - var endpoints *oidcclient.Discovery - - ginkgo.It("completes full OIDC authorization code flow via browser", func() { - verifier, challenge, err := oidcclient.GeneratePKCE() - Expect(err).ToNot(HaveOccurred()) - - state := oidcclient.RandomBase64(16) - nonce := oidcclient.RandomBase64(16) - - endpoints, err = oidcclient.Discover(serverURL + "/.well-known/openid-configuration") - Expect(err).ToNot(HaveOccurred()) - Expect(endpoints.AuthorizationEndpoint).ToNot(BeEmpty()) - Expect(endpoints.TokenEndpoint).ToNot(BeEmpty()) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - Expect(err).ToNot(HaveOccurred()) - callbackPort := listener.Addr().(*net.TCPAddr).Port - redirectURI := fmt.Sprintf("http://127.0.0.1:%d/callback", callbackPort) - - codeCh := make(chan string, 1) - errCh := make(chan error, 1) - callbackServer := &http.Server{} - callbackServer.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/callback" { - http.NotFound(w, r) - return - } - q := r.URL.Query() - if s := q.Get("state"); s != state { - errCh <- fmt.Errorf("state mismatch: got %s", s) - http.Error(w, "state mismatch", http.StatusBadRequest) - return - } - if e := q.Get("error"); e != "" { - errCh <- fmt.Errorf("auth error: %s: %s", e, q.Get("error_description")) - http.Error(w, e, http.StatusBadRequest) - return - } - code := q.Get("code") - if code == "" { - errCh <- fmt.Errorf("missing code") - http.Error(w, "missing code", http.StatusBadRequest) - return - } - fmt.Fprint(w, "Login successful") - codeCh <- code - }) - go func() { - if err := callbackServer.Serve(listener); err != nil && err != http.ErrServerClosed { - errCh <- err - } - }() - defer func() { _ = callbackServer.Shutdown(context.Background()) }() - - authURL := fmt.Sprintf("%s?client_id=mc-cli&response_type=code&scope=%s&redirect_uri=%s&state=%s&nonce=%s&code_challenge=%s&code_challenge_method=S256", - endpoints.AuthorizationEndpoint, - url.QueryEscape("openid profile email offline_access"), - url.QueryEscape(redirectURI), - url.QueryEscape(state), - url.QueryEscape(nonce), - url.QueryEscape(challenge), - ) - - err = chromedp.Run(chromectx, - chromedp.Navigate(authURL), - chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery), - chromedp.SendKeys(`input[name="username"]`, "admin", chromedp.ByQuery), - chromedp.SendKeys(`input[name="password"]`, "admin", chromedp.ByQuery), - chromedp.Click(`button[type="submit"]`, chromedp.ByQuery), - ) - Expect(err).ToNot(HaveOccurred()) - - var code string - select { - case code = <-codeCh: - case err := <-errCh: - ginkgo.Fail(fmt.Sprintf("callback error: %v", err)) - case <-time.After(30 * time.Second): - ginkgo.Fail("timed out waiting for callback") - } - Expect(code).ToNot(BeEmpty()) - - tokens, err = oidcclient.ExchangeCode(endpoints.TokenEndpoint, code, redirectURI, verifier) - Expect(err).ToNot(HaveOccurred()) - Expect(tokens.AccessToken).ToNot(BeEmpty()) - Expect(tokens.IDToken).ToNot(BeEmpty()) - Expect(tokens.RefreshToken).ToNot(BeEmpty()) - - Expect(oidcclient.ValidateNonce(tokens.IDToken, nonce)).To(Succeed()) - - req, err := http.NewRequest("GET", serverURL+"/userinfo", nil) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Authorization", "Bearer "+tokens.AccessToken) - - resp, err := http.DefaultClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - resp.Body.Close() - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - }) - - ginkgo.It("refreshes tokens using the refresh token", func() { - Expect(tokens).ToNot(BeNil(), "login test must run first") - Expect(endpoints).ToNot(BeNil()) - - originalAccess := tokens.AccessToken - originalRefresh := tokens.RefreshToken - - refreshed, err := oidcclient.RefreshToken(endpoints.TokenEndpoint, originalRefresh) - Expect(err).ToNot(HaveOccurred()) - Expect(refreshed.AccessToken).ToNot(BeEmpty()) - Expect(refreshed.RefreshToken).ToNot(BeEmpty()) - Expect(refreshed.AccessToken).ToNot(Equal(originalAccess)) - - // Verify new access token works - req, err := http.NewRequest("GET", serverURL+"/userinfo", nil) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Authorization", "Bearer "+refreshed.AccessToken) - - resp, err := http.DefaultClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - resp.Body.Close() - Expect(resp.StatusCode).To(Equal(http.StatusOK)) - }) -}) diff --git a/tests/oidc_e2e/suite_test.go b/tests/oidc_e2e/suite_test.go deleted file mode 100644 index cf2959953..000000000 --- a/tests/oidc_e2e/suite_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package oidc_e2e - -import ( - "context" - "fmt" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "strconv" - "syscall" - "testing" - "time" - - "github.com/chromedp/chromedp" - ginkgo "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "golang.org/x/crypto/bcrypt" -) - -var ( - serverURL string - serverPort int - serverCmd *exec.Cmd - chromectx context.Context - chromeCanc context.CancelFunc - tmpDir string -) - -func TestOIDCE2E(t *testing.T) { - RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, "OIDC E2E") -} - -func freePort() int { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - panic(err) - } - port := l.Addr().(*net.TCPAddr).Port - l.Close() - return port -} - -var _ = ginkgo.BeforeSuite(func() { - serverPort = freePort() - serverURL = fmt.Sprintf("http://localhost:%d", serverPort) - - tmpDir = ginkgo.GinkgoT().TempDir() - - hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost) - Expect(err).ToNot(HaveOccurred()) - htpasswdPath := filepath.Join(tmpDir, "htpasswd") - Expect(os.WriteFile(htpasswdPath, fmt.Appendf(nil, "admin:%s\n", string(hash)), 0600)).To(Succeed()) - - dbPath := filepath.Join(tmpDir, ".db") - Expect(os.MkdirAll(dbPath, 0750)).To(Succeed()) - - binPath, err := filepath.Abs(".bin/incident-commander") - if err != nil { - binPath = ".bin/incident-commander" - } - if _, err := os.Stat(binPath); os.IsNotExist(err) { - // Try from project root - binPath, _ = filepath.Abs("../../.bin/incident-commander") - } - Expect(binPath).To(BeAnExistingFile(), "binary not found — run 'make dev' first") - - serverCmd = exec.Command(binPath, "serve", - "--db", fmt.Sprintf("embedded://%s", dbPath), - "--auth", "basic", - "--htpasswd-file", htpasswdPath, - "--oidc", - "--public-endpoint", serverURL, - "--httpPort", strconv.Itoa(serverPort), - "--disable-postgrest", - "--postgrest-uri", "", - "--disable-operators", - "--disable-kubernetes", - ) - serverCmd.Stdout = ginkgo.GinkgoWriter - serverCmd.Stderr = ginkgo.GinkgoWriter - serverCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - Expect(serverCmd.Start()).To(Succeed()) - - Eventually(func() error { - resp, err := http.Get(serverURL + "/health") - if err != nil { - return err - } - resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("health returned %d", resp.StatusCode) - } - return nil - }).WithTimeout(90 * time.Second).WithPolling(time.Second).Should(Succeed()) - - chromectx, chromeCanc = chromedp.NewContext(context.Background()) -}) - -var _ = ginkgo.AfterSuite(func() { - if chromeCanc != nil { - chromeCanc() - } - if serverCmd != nil && serverCmd.Process != nil { - _ = syscall.Kill(-serverCmd.Process.Pid, syscall.SIGTERM) - _ = serverCmd.Wait() - } -}) From 85f50524478f2d7b0f19780256f57b1efd1cdf49 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 30 Mar 2026 18:27:45 +0300 Subject: [PATCH 19/48] refactor: extract shared facet rendering backend into report package Move duplicated facetSrcDir, extractReportFiles, and CLI/HTTP rendering logic from application/, rbac_report/, and views/ into report/facet.go. Each package now delegates to report.RenderCLI or report.RenderHTTP. --- application/render_facet.go | 120 +-------------- rbac_report/render_facet.go | 95 +----------- report/facet.go | 232 +++++++++++++++++++++++++++++ views/render_facet.go | 289 ++++-------------------------------- views/render_facet_test.go | 15 +- 5 files changed, 272 insertions(+), 479 deletions(-) create mode 100644 report/facet.go diff --git a/application/render_facet.go b/application/render_facet.go index 59ba766fe..3ba188fe4 100644 --- a/application/render_facet.go +++ b/application/render_facet.go @@ -1,116 +1,26 @@ package application import ( - "bytes" - "encoding/json" "fmt" - "io/fs" - "os" - "os/exec" - "path/filepath" - "strings" icapi "github.com/flanksource/incident-commander/api" "github.com/flanksource/incident-commander/report" ) func RenderFacetHTML(app *icapi.Application) ([]byte, error) { - return renderWithFacet(app, "html") -} - -func RenderFacetPDF(app *icapi.Application) ([]byte, error) { - return renderWithFacet(app, "pdf") -} - -func renderWithFacet(app *icapi.Application, format string) ([]byte, error) { if app == nil { return nil, fmt.Errorf("application must not be nil") } - - facetBin, err := exec.LookPath("facet") - if err != nil { - return nil, fmt.Errorf("facet not found on PATH: install with 'npm install -g @flanksource/facet'") - } - - srcDir, err := facetSrcDir() - if err != nil { - return nil, fmt.Errorf("prepare facet src dir: %w", err) - } - - dataJSON, err := json.MarshalIndent(initSlices(app), "", " ") - if err != nil { - return nil, fmt.Errorf("marshal application: %w", err) - } - - dataFile, err := os.CreateTemp("", "facet-data-*.json") - if err != nil { - return nil, fmt.Errorf("create data temp file: %w", err) - } - defer os.Remove(dataFile.Name()) - - if _, err := dataFile.Write(dataJSON); err != nil { - return nil, fmt.Errorf("write data file: %w", err) - } - dataFile.Close() - - ext := format - if format == "html" { - ext = "html" - } - - outFile, err := os.CreateTemp("", "facet-output-*."+ext) - if err != nil { - return nil, fmt.Errorf("create output temp file: %w", err) - } - outFile.Close() - defer os.Remove(outFile.Name()) - - var stderr bytes.Buffer - cmd := exec.Command(facetBin, format, "Application.tsx", "-d", dataFile.Name(), "-o", outFile.Name()) - cmd.Dir = srcDir - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("facet %s failed: %w\n%s", format, err, stderr.String()) - } - - data, err := os.ReadFile(outFile.Name()) - if err != nil { - entries, _ := os.ReadDir(srcDir) - names := make([]string, 0, len(entries)) - for _, e := range entries { - names = append(names, e.Name()) - } - return nil, fmt.Errorf("read facet output %s: %w (srcDir contains: %s)", outFile.Name(), err, strings.Join(names, ", ")) - } - - return data, nil + return report.RenderCLI(initSlices(app), "html", "Application.tsx") } -// facetSrcDir returns a stable directory containing the embedded report TSX files. -// On first call it extracts the files; subsequent calls reuse the directory so that -// facet can cache its .facet/node_modules there across invocations. -func facetSrcDir() (string, error) { - cacheDir, err := os.UserCacheDir() - if err != nil { - cacheDir = os.TempDir() - } - dir := filepath.Join(cacheDir, "incident-commander", "facet-report") - - if err := os.MkdirAll(dir, 0750); err != nil { - return "", fmt.Errorf("create cache dir: %w", err) - } - - // Always (re)extract so embedded changes are picked up on binary upgrade. - if err := extractReportFiles(dir); err != nil { - return "", err +func RenderFacetPDF(app *icapi.Application) ([]byte, error) { + if app == nil { + return nil, fmt.Errorf("application must not be nil") } - - return dir, nil + return report.RenderCLI(initSlices(app), "pdf", "Application.tsx") } -// initSlices returns a shallow copy of app with nil slices replaced by empty -// slices so the TSX renderer receives [] instead of null. func initSlices(app *icapi.Application) icapi.Application { out := *app if out.Incidents == nil { @@ -139,23 +49,3 @@ func initSlices(app *icapi.Application) icapi.Application { } return out } - -func extractReportFiles(destDir string) error { - return fs.WalkDir(report.FS, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." { - return nil - } - dest := filepath.Join(destDir, path) - if d.IsDir() { - return os.MkdirAll(dest, 0750) - } - data, err := report.FS.ReadFile(path) - if err != nil { - return err - } - return os.WriteFile(dest, data, 0600) - }) -} diff --git a/rbac_report/render_facet.go b/rbac_report/render_facet.go index a13c580b3..54cd192dc 100644 --- a/rbac_report/render_facet.go +++ b/rbac_report/render_facet.go @@ -1,13 +1,7 @@ package rbac_report import ( - "bytes" - "encoding/json" "fmt" - "io/fs" - "os" - "os/exec" - "path/filepath" "github.com/flanksource/duty/context" @@ -28,105 +22,22 @@ func renderWithFacet(ctx context.Context, r *api.RBACReport, format string, byUs return nil, fmt.Errorf("RBAC report must not be nil") } - facetBin, err := exec.LookPath("facet") - if err != nil { - return nil, fmt.Errorf("facet not found on PATH: install with 'npm install -g @flanksource/facet'") - } - - srcDir, err := facetSrcDir() - if err != nil { - return nil, fmt.Errorf("prepare facet src dir: %w", err) - } - - dataJSON, err := json.MarshalIndent(initSlices(r), "", " ") - if err != nil { - return nil, fmt.Errorf("marshal RBAC report: %w", err) - } - - ctx.Logger.V(3).Infof("facet binary: %s, data size: %dKB", facetBin, len(dataJSON)/1024) - - dataFile, err := os.CreateTemp("", "facet-rbac-data-*.json") - if err != nil { - return nil, fmt.Errorf("create data temp file: %w", err) - } - defer os.Remove(dataFile.Name()) - - if _, err := dataFile.Write(dataJSON); err != nil { - return nil, fmt.Errorf("write data file: %w", err) - } - dataFile.Close() - - outFile, err := os.CreateTemp("", "facet-rbac-output-*."+format) - if err != nil { - return nil, fmt.Errorf("create output temp file: %w", err) - } - outFile.Close() - defer os.Remove(outFile.Name()) - entryFile := "RBACReport.tsx" if byUser { entryFile = "RBACByUserReport.tsx" } - var stderr, stdout bytes.Buffer - cmd := exec.Command(facetBin, format, entryFile, "-d", dataFile.Name(), "-o", outFile.Name()) - cmd.Dir = srcDir - cmd.Stderr = &stderr - cmd.Stdout = &stdout + ctx.Logger.V(3).Infof("Rendering facet-%s", format) - ctx.Logger.V(3).Infof("Rendering facet-%s (%dKB data)", format, len(dataJSON)/1024) - - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("facet %s failed: %w\n%s", format, err, stderr.String()) - } - - result, err := os.ReadFile(outFile.Name()) + result, err := report.RenderCLI(initSlices(r), format, entryFile) if err != nil { - return nil, fmt.Errorf("read facet output: %w", err) + return nil, err } ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result)/1024, format) return result, nil } -func facetSrcDir() (string, error) { - cacheDir, err := os.UserCacheDir() - if err != nil { - cacheDir = os.TempDir() - } - dir := filepath.Join(cacheDir, "incident-commander", "facet-report") - - if err := os.MkdirAll(dir, 0750); err != nil { - return "", fmt.Errorf("create cache dir: %w", err) - } - - if err := extractReportFiles(dir); err != nil { - return "", err - } - - return dir, nil -} - -func extractReportFiles(destDir string) error { - return fs.WalkDir(report.FS, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." { - return nil - } - dest := filepath.Join(destDir, path) - if d.IsDir() { - return os.MkdirAll(dest, 0750) - } - data, err := report.FS.ReadFile(path) - if err != nil { - return err - } - return os.WriteFile(dest, data, 0600) - }) -} - func initSlices(r *api.RBACReport) api.RBACReport { out := *r if out.Resources == nil { diff --git a/report/facet.go b/report/facet.go new file mode 100644 index 000000000..da62dd6f0 --- /dev/null +++ b/report/facet.go @@ -0,0 +1,232 @@ +package report + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "io/fs" + "mime/multipart" + "os" + "os/exec" + "path/filepath" + "sync" + + commonshttp "github.com/flanksource/commons/http" + "github.com/flanksource/duty/context" +) + +// RenderCLI renders data to the given format using the local facet CLI binary. +func RenderCLI(data any, format, entryFile string) ([]byte, error) { + facetBin, err := exec.LookPath("facet") + if err != nil { + return nil, fmt.Errorf("facet not found on PATH: install with 'npm install -g @flanksource/facet'") + } + + srcDir, err := SrcDir() + if err != nil { + return nil, fmt.Errorf("prepare facet src dir: %w", err) + } + + dataJSON, err := json.MarshalIndent(data, "", " ") + if err != nil { + return nil, fmt.Errorf("marshal data: %w", err) + } + + dataFile, err := os.CreateTemp("", "facet-data-*.json") + if err != nil { + return nil, fmt.Errorf("create data temp file: %w", err) + } + defer os.Remove(dataFile.Name()) + + if _, err := dataFile.Write(dataJSON); err != nil { + return nil, fmt.Errorf("write data file: %w", err) + } + dataFile.Close() + + outFile, err := os.CreateTemp("", "facet-output-*."+format) + if err != nil { + return nil, fmt.Errorf("create output temp file: %w", err) + } + outFile.Close() + defer os.Remove(outFile.Name()) + + var stderr bytes.Buffer + cmd := exec.Command(facetBin, format, entryFile, "-d", dataFile.Name(), "-o", outFile.Name()) + cmd.Dir = srcDir + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("facet %s failed: %w\n%s", format, err, stderr.String()) + } + + return os.ReadFile(outFile.Name()) +} + +// RenderHTTP renders data via a remote facet rendering service. +func RenderHTTP(ctx context.Context, baseURL, token string, data any, format, entryFile string) ([]byte, error) { + archive, err := BuildArchive() + if err != nil { + return nil, fmt.Errorf("build report archive: %w", err) + } + + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("marshal data: %w", err) + } + + optionsJSON, err := json.Marshal(map[string]any{ + "format": format, + "entryFile": entryFile, + }) + if err != nil { + return nil, fmt.Errorf("marshal options: %w", err) + } + + var body bytes.Buffer + mw := multipart.NewWriter(&body) + + fw, err := mw.CreateFormFile("archive", "report.tar.gz") + if err != nil { + return nil, fmt.Errorf("create archive form field: %w", err) + } + if _, err := fw.Write(archive); err != nil { + return nil, fmt.Errorf("write archive field: %w", err) + } + + if err := mw.WriteField("data", string(dataJSON)); err != nil { + return nil, fmt.Errorf("write data field: %w", err) + } + + if err := mw.WriteField("options", string(optionsJSON)); err != nil { + return nil, fmt.Errorf("write options field: %w", err) + } + + if err := mw.Close(); err != nil { + return nil, fmt.Errorf("close multipart writer: %w", err) + } + + client := commonshttp.NewClient().BaseURL(baseURL) + if token != "" { + client = client.Header("X-API-Key", token) + } + + response, err := client.R(ctx). + Header("Content-Type", mw.FormDataContentType()). + Post("/render", &body) + if err != nil { + return nil, fmt.Errorf("facet render request failed: %w", err) + } + if !response.IsOK() { + errBody, _ := response.AsString() + return nil, fmt.Errorf("facet render failed (status %d): %s", response.StatusCode, errBody) + } + + if format == "html" { + return io.ReadAll(response.Body) + } + + renderResult, err := response.AsJSON() + if err != nil { + return nil, fmt.Errorf("failed to parse render response: %w", err) + } + resultURL, _ := renderResult["url"].(string) + if resultURL == "" { + return nil, fmt.Errorf("render response missing 'url' field") + } + + pdfResponse, err := client.R(ctx).Get(resultURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch rendered result: %w", err) + } + if !pdfResponse.IsOK() { + errBody, _ := pdfResponse.AsString() + return nil, fmt.Errorf("result fetch failed (status %d): %s", pdfResponse.StatusCode, errBody) + } + + return io.ReadAll(pdfResponse.Body) +} + +// SrcDir returns a stable directory containing the embedded report TSX files. +// On first call it extracts the files; subsequent calls reuse the directory. +var SrcDir = sync.OnceValues(func() (string, error) { + if SourceDir != "" { + return SourceDir, nil + } + + cacheDir, err := os.UserCacheDir() + if err != nil { + cacheDir = os.TempDir() + } + dir := filepath.Join(cacheDir, "incident-commander", "facet-report") + + if err := os.MkdirAll(dir, 0750); err != nil { + return "", fmt.Errorf("create cache dir: %w", err) + } + + if err := ExtractFiles(dir); err != nil { + return "", err + } + + return dir, nil +}) + +// ExtractFiles writes all embedded report files to destDir. +func ExtractFiles(destDir string) error { + return fs.WalkDir(FS, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if path == "." { + return nil + } + dest := filepath.Join(destDir, path) + if d.IsDir() { + return os.MkdirAll(dest, 0750) + } + data, err := FS.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(dest, data, 0600) + }) +} + +// BuildArchive creates a tar.gz archive of all embedded report files. +func BuildArchive() ([]byte, error) { + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gw) + + err := fs.WalkDir(FS, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + data, err := FS.ReadFile(path) + if err != nil { + return err + } + if err := tw.WriteHeader(&tar.Header{ + Name: path, + Size: int64(len(data)), + Mode: 0600, + }); err != nil { + return err + } + _, err = tw.Write(data) + return err + }) + if err != nil { + return nil, err + } + + if err := tw.Close(); err != nil { + return nil, err + } + if err := gw.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/views/render_facet.go b/views/render_facet.go index 6568d23c1..776182ab0 100644 --- a/views/render_facet.go +++ b/views/render_facet.go @@ -1,20 +1,8 @@ package views import ( - "archive/tar" - "bytes" - "compress/gzip" - "encoding/json" "fmt" - "io" - "io/fs" - "mime/multipart" - "os" - "os/exec" - "path/filepath" - "sync" - commonshttp "github.com/flanksource/commons/http" "github.com/flanksource/duty/connection" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" @@ -32,10 +20,8 @@ type facetViewSectionResult struct { View *facetViewPayload `json:"view,omitempty"` } -// facetViewPayload is used only for facet-html/facet-pdf rendering. -// api.ViewResult keeps Rows as json:"-" for regular API responses, but the facet -// report TSX reads table data from `rows`, so we inject Rows here without changing -// the public API shape. SectionResults is also wrapped so nested viewRef tables keep rows. +// facetViewPayload injects Rows into the JSON for facet rendering. +// api.ViewResult keeps Rows as json:"-" for regular API responses. type facetViewPayload struct { *api.ViewResult Rows []view.Row `json:"rows,omitempty"` @@ -106,6 +92,32 @@ func RenderMultiFacetPDF(ctx context.Context, multi *api.MultiViewResult, opts * return renderFacetWithData(ctx, newFacetMultiViewPayload(multi), "pdf", opts) } +const viewEntryFile = "ViewReport.tsx" + +func renderViewWithFacet(ctx context.Context, result *api.ViewResult, format string, opts *v1.FacetOptions) ([]byte, error) { + if result == nil { + return nil, fmt.Errorf("view result must not be nil") + } + return renderFacetWithData(ctx, newFacetViewPayload(result), format, opts) +} + +func renderFacetWithData(ctx context.Context, data any, format string, opts *v1.FacetOptions) ([]byte, error) { + if data == nil { + return nil, fmt.Errorf("data must not be nil") + } + + baseURL, token, _, err := resolveFacetConnection(ctx, opts) + if err != nil { + return nil, err + } + + if baseURL != "" { + return report.RenderHTTP(ctx, baseURL, token, data, format, viewEntryFile) + } + + return report.RenderCLI(data, format, viewEntryFile) +} + func resolveFacetConnection(ctx context.Context, opts *v1.FacetOptions) (baseURL, token, timestampURL string, err error) { if opts == nil { return "", "", "", nil @@ -137,248 +149,3 @@ func resolveFacetConnection(ctx context.Context, opts *v1.FacetOptions) (baseURL return baseURL, token, timestampURL, nil } - -func buildReportArchive() ([]byte, error) { - var buf bytes.Buffer - gw := gzip.NewWriter(&buf) - tw := tar.NewWriter(gw) - - err := fs.WalkDir(report.FS, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil || d.IsDir() { - return err - } - data, err := report.FS.ReadFile(path) - if err != nil { - return err - } - if err := tw.WriteHeader(&tar.Header{ - Name: path, - Size: int64(len(data)), - Mode: 0600, - }); err != nil { - return err - } - _, err = tw.Write(data) - return err - }) - if err != nil { - return nil, err - } - - if err := tw.Close(); err != nil { - return nil, err - } - if err := gw.Close(); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func renderFacetHTTP(ctx context.Context, baseURL, token string, data any, format string, opts *v1.FacetOptions) ([]byte, error) { - archive, err := buildReportArchive() - if err != nil { - return nil, fmt.Errorf("build report archive: %w", err) - } - - dataJSON, err := json.Marshal(data) - if err != nil { - return nil, fmt.Errorf("marshal data: %w", err) - } - - optionsJSON, err := json.Marshal(map[string]any{ - "format": format, - "entryFile": "ViewReport.tsx", - }) - if err != nil { - return nil, fmt.Errorf("marshal options: %w", err) - } - - var body bytes.Buffer - mw := multipart.NewWriter(&body) - - fw, err := mw.CreateFormFile("archive", "report.tar.gz") - if err != nil { - return nil, fmt.Errorf("create archive form field: %w", err) - } - if _, err := fw.Write(archive); err != nil { - return nil, fmt.Errorf("write archive field: %w", err) - } - - if err := mw.WriteField("data", string(dataJSON)); err != nil { - return nil, fmt.Errorf("write data field: %w", err) - } - - if err := mw.WriteField("options", string(optionsJSON)); err != nil { - return nil, fmt.Errorf("write options field: %w", err) - } - - if err := mw.Close(); err != nil { - return nil, fmt.Errorf("close multipart writer: %w", err) - } - - client := commonshttp.NewClient().BaseURL(baseURL) - if token != "" { - client = client.Header("X-API-Key", token) - } - - response, err := client.R(ctx). - Header("Content-Type", mw.FormDataContentType()). - Post("/render", &body) - if err != nil { - return nil, fmt.Errorf("facet render request failed: %w", err) - } - if !response.IsOK() { - errBody, _ := response.AsString() - return nil, fmt.Errorf("facet render failed (status %d): %s", response.StatusCode, errBody) - } - - if format == "html" { - return io.ReadAll(response.Body) - } - - renderResult, err := response.AsJSON() - if err != nil { - return nil, fmt.Errorf("failed to parse render response: %w", err) - } - resultURL, _ := renderResult["url"].(string) - if resultURL == "" { - return nil, fmt.Errorf("render response missing 'url' field") - } - - pdfResponse, err := client.R(ctx).Get(resultURL) - if err != nil { - return nil, fmt.Errorf("failed to fetch rendered result: %w", err) - } - if !pdfResponse.IsOK() { - errBody, _ := pdfResponse.AsString() - return nil, fmt.Errorf("result fetch failed (status %d): %s", pdfResponse.StatusCode, errBody) - } - - return io.ReadAll(pdfResponse.Body) -} - -func renderFacetWithData(ctx context.Context, data any, format string, opts *v1.FacetOptions) ([]byte, error) { - if data == nil { - return nil, fmt.Errorf("data must not be nil") - } - - baseURL, token, _, err := resolveFacetConnection(ctx, opts) - if err != nil { - return nil, err - } - if baseURL != "" { - return renderFacetHTTP(ctx, baseURL, token, data, format, opts) - } - - return renderFacetCLI(ctx, data, format) -} - -func renderViewWithFacet(ctx context.Context, result *api.ViewResult, format string, opts *v1.FacetOptions) ([]byte, error) { - if result == nil { - return nil, fmt.Errorf("view result must not be nil") - } - - payload := newFacetViewPayload(result) - - baseURL, token, _, err := resolveFacetConnection(ctx, opts) - if err != nil { - return nil, err - } - if baseURL != "" { - return renderFacetHTTP(ctx, baseURL, token, payload, format, opts) - } - - return renderFacetCLI(ctx, payload, format) -} - -func renderFacetCLI(ctx context.Context, data any, format string) ([]byte, error) { - facetBin, err := exec.LookPath("facet") - if err != nil { - return nil, fmt.Errorf("facet not found on PATH: install with 'npm install -g @flanksource/facet'") - } - - srcDir, err := viewFacetSrcDir() - if err != nil { - return nil, fmt.Errorf("prepare facet src dir: %w", err) - } - - dataJSON, err := json.MarshalIndent(data, "", " ") - if err != nil { - return nil, fmt.Errorf("marshal data: %w", err) - } - - dataFile, err := os.CreateTemp("", "facet-view-data-*.json") - if err != nil { - return nil, fmt.Errorf("create data temp file: %w", err) - } - defer os.Remove(dataFile.Name()) - - if _, err := dataFile.Write(dataJSON); err != nil { - return nil, fmt.Errorf("write data file: %w", err) - } - dataFile.Close() - - outFile, err := os.CreateTemp("", "facet-view-output-*."+format) - if err != nil { - return nil, fmt.Errorf("create output temp file: %w", err) - } - outFile.Close() - defer os.Remove(outFile.Name()) - - ctx.Logger.V(3).Infof("facet binary: %s, data size: %dKB", facetBin, len(dataJSON)/1024) - - var stderr bytes.Buffer - cmd := exec.Command(facetBin, format, "ViewReport.tsx", "-d", dataFile.Name(), "-o", outFile.Name()) - cmd.Dir = srcDir - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("facet %s failed: %w\n%s", format, err, stderr.String()) - } - - result, err := os.ReadFile(outFile.Name()) - if err != nil { - return nil, fmt.Errorf("read facet output: %w", err) - } - - ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result)/1024, format) - return result, nil -} - -var viewFacetSrcDir = sync.OnceValues(func() (string, error) { - cacheDir, err := os.UserCacheDir() - if err != nil { - cacheDir = os.TempDir() - } - dir := filepath.Join(cacheDir, "incident-commander", "facet-report") - - if err := os.MkdirAll(dir, 0750); err != nil { - return "", fmt.Errorf("create cache dir: %w", err) - } - - if err := viewExtractReportFiles(dir); err != nil { - return "", err - } - - return dir, nil -}) - -func viewExtractReportFiles(destDir string) error { - return fs.WalkDir(report.FS, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." { - return nil - } - dest := filepath.Join(destDir, path) - if d.IsDir() { - return os.MkdirAll(dest, 0750) - } - data, err := report.FS.ReadFile(path) - if err != nil { - return err - } - return os.WriteFile(dest, data, 0600) - }) -} diff --git a/views/render_facet_test.go b/views/render_facet_test.go index 8fa7ee8c3..b098f5a10 100644 --- a/views/render_facet_test.go +++ b/views/render_facet_test.go @@ -9,13 +9,13 @@ import ( ginkgo "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - v1 "github.com/flanksource/incident-commander/api/v1" + "github.com/flanksource/incident-commander/report" ) // maxMultipartMemory is the max memory used when parsing multipart form data in tests (32MB). const maxMultipartMemory = 32 << 20 -var _ = ginkgo.Describe("renderFacetHTTP", func() { +var _ = ginkgo.Describe("RenderHTTP", func() { var ( server *httptest.Server pdfBytes = []byte("%PDF-1.4 test content") @@ -66,14 +66,7 @@ var _ = ginkgo.Describe("renderFacetHTTP", func() { }) ginkgo.It("fetches PDF via two-step render+download", func() { - opts := &v1.FacetOptions{ - URL: server.URL, - PDFOptions: &v1.FacetPDFOptions{ - PageSize: "A4", - }, - } - - result, err := renderFacetHTTP(DefaultContext, server.URL, "test-token", map[string]string{"key": "value"}, "pdf", opts) + result, err := report.RenderHTTP(DefaultContext, server.URL, "test-token", map[string]string{"key": "value"}, "pdf", "ViewReport.tsx") Expect(err).ToNot(HaveOccurred()) Expect(result).To(Equal(pdfBytes)) }) @@ -92,7 +85,7 @@ var _ = ginkgo.Describe("renderFacetHTTP", func() { })) defer htmlServer.Close() - result, err := renderFacetHTTP(DefaultContext, htmlServer.URL, "", map[string]string{"key": "value"}, "html", nil) + result, err := report.RenderHTTP(DefaultContext, htmlServer.URL, "", map[string]string{"key": "value"}, "html", "ViewReport.tsx") Expect(err).ToNot(HaveOccurred()) Expect(result).To(Equal(htmlBytes)) }) From e85e510127133fe2283100fe58136998cb9dcf3b Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 31 Mar 2026 06:27:27 +0300 Subject: [PATCH 20/48] refactor(report): optimize report layouts and styling for better pdf rendering Adjust page margins, reduce header/footer sizes, switch RBAC reports to landscape orientation, replace date formatting for consistency, refactor changelog and role source into custom badge components with color coding, remove health color references, add legend section, and update facet dependency to support pdf-lib for improved rendering --- report/Application.tsx | 2 +- report/RBACByUserReport.tsx | 33 +- report/RBACReport.tsx | 33 +- report/ViewReport.tsx | 2 +- report/components/RBACChangelogSection.tsx | 58 +- report/components/RBACResourceSection.tsx | 130 +- report/components/RBACUserSection.tsx | 40 +- report/embed.go | 5 + report/package-lock.json | 1887 ++++---------------- report/package.json | 2 +- 10 files changed, 573 insertions(+), 1619 deletions(-) diff --git a/report/Application.tsx b/report/Application.tsx index e08b21bd6..a1741f368 100644 --- a/report/Application.tsx +++ b/report/Application.tsx @@ -71,7 +71,7 @@ export default function ApplicationReport({ data }: ApplicationReportProps) { const footer = ; const pageProps = { pageSize: 'a4' as const, - margins: { top: 5, bottom: 5, left: 5, right: 5 }, + margins: { top: 5, bottom: 5, left: 10, right: 10 }, header, headerHeight: 10, footer, diff --git a/report/RBACByUserReport.tsx b/report/RBACByUserReport.tsx index 150fb39b9..5e32879dd 100644 --- a/report/RBACByUserReport.tsx +++ b/report/RBACByUserReport.tsx @@ -7,7 +7,7 @@ import RBACChangelogSection from './components/RBACChangelogSection.tsx'; function PageHeader({ title }: { title: string }) { return ( -
+
{title} RBAC Report (By User)
@@ -15,22 +15,18 @@ function PageHeader({ title }: { title: string }) { } function PageFooter() { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); return ( -
- Generated {date} +
+ Generated {now}
); } function CoverPage({ title, query }: { title: string; query?: string }) { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); return ( -
+
RBAC Report - By User @@ -45,7 +41,7 @@ function CoverPage({ title, query }: { title: string; query?: string }) { )}
-
Generated on {date}
+
Generated {now}
); } @@ -58,19 +54,20 @@ export default function RBACByUserReportPage({ data }: Props) { const header = ; const footer = ; const pageProps = { - pageSize: 'a4' as const, - margins: { top: 3, bottom: 3, left: 3, right: 3 }, + pageSize: 'a4-landscape' as const, + margins: { top: 1, bottom: 1, left: 0, right: 0 }, header, - headerHeight: 10, + headerHeight: 8, footer, - footerHeight: 10, + footerHeight: 8, }; const users = data.users || []; return ( <> - + + @@ -84,9 +81,7 @@ export default function RBACByUserReportPage({ data }: Props) { -
- -
+
))} diff --git a/report/RBACReport.tsx b/report/RBACReport.tsx index c22925bee..e65c9a9c0 100644 --- a/report/RBACReport.tsx +++ b/report/RBACReport.tsx @@ -7,7 +7,7 @@ import RBACChangelogSection from './components/RBACChangelogSection.tsx'; function PageHeader({ title }: { title: string }) { return ( -
+
{title} RBAC Report
@@ -15,22 +15,18 @@ function PageHeader({ title }: { title: string }) { } function PageFooter() { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); return ( -
- Generated {date} +
+ Generated {now}
); } function CoverPage({ title, query }: { title: string; query?: string }) { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); return ( -
+
RBAC Report @@ -45,7 +41,7 @@ function CoverPage({ title, query }: { title: string; query?: string }) { )}
-
Generated on {date}
+
Generated {now}
); } @@ -58,17 +54,18 @@ export default function RBACReportPage({ data }: RBACReportProps) { const header = ; const footer = ; const pageProps = { - pageSize: 'a4' as const, - margins: { top: 3, bottom: 3, left: 3, right: 3 }, + pageSize: 'a4-landscape' as const, + margins: { top: 1, bottom: 1, left: 0, right: 0 }, header, - headerHeight: 10, + headerHeight: 8, footer, - footerHeight: 10, + footerHeight: 8, }; return ( <> - + + @@ -82,9 +79,7 @@ export default function RBACReportPage({ data }: RBACReportProps) { -
- -
+
))} diff --git a/report/ViewReport.tsx b/report/ViewReport.tsx index f225b4df3..16286934d 100644 --- a/report/ViewReport.tsx +++ b/report/ViewReport.tsx @@ -80,7 +80,7 @@ export default function ViewReportPage({ data }: ViewReportProps) { const footer = ; const pageProps = { pageSize: 'a4' as const, - margins: { top: 3, bottom: 3, left: 3, right: 3 }, + margins: { top: 3, bottom: 3, left: 10, right: 10 }, header, headerHeight: 10, footer, diff --git a/report/components/RBACChangelogSection.tsx b/report/components/RBACChangelogSection.tsx index 3bee343e0..0327cb6cf 100644 --- a/report/components/RBACChangelogSection.tsx +++ b/report/components/RBACChangelogSection.tsx @@ -1,8 +1,26 @@ import React from 'react'; -import { Section, CompactTable } from '@flanksource/facet'; +import { Section } from '@flanksource/facet'; import type { RBACChangeEntry } from '../rbac-types.ts'; import { formatDate } from './utils.ts'; +const CHANGELOG_TYPE_COLORS: Record = { + PermissionGranted: { bg: '#DCFCE7', fg: '#166534' }, + PermissionRevoked: { bg: '#FEE2E2', fg: '#991B1B' }, + AccessReviewed: { bg: '#DBEAFE', fg: '#1E40AF' }, +}; + +function ChangeTypeBadge({ type }: { type: string }) { + const colors = CHANGELOG_TYPE_COLORS[type] || { bg: '#E2E8F0', fg: '#334155' }; + return ( + + {type} + + ); +} + interface Props { changelog: RBACChangeEntry[]; } @@ -10,23 +28,31 @@ interface Props { export default function RBACChangelogSection({ changelog }: Props) { if (changelog.length === 0) return null; - const rows = changelog.map((entry) => [ - formatDate(entry.date), - entry.changeType, - entry.user, - entry.role, - entry.configName, - entry.source, - entry.description, - ]); - return (
- +
+ Legend: + {Object.entries(CHANGELOG_TYPE_COLORS).map(([key, colors]) => ( + + + {key} + + ))} +
+
+ {changelog.map((entry, i) => ( +
+ {formatDate(entry.date)} + + {entry.user} + + {entry.role} + {entry.configName} + {entry.source && ({entry.source})} + {entry.description && {entry.description}} +
+ ))} +
); } diff --git a/report/components/RBACResourceSection.tsx b/report/components/RBACResourceSection.tsx index 417e3887b..4f1aa6341 100644 --- a/report/components/RBACResourceSection.tsx +++ b/report/components/RBACResourceSection.tsx @@ -2,9 +2,19 @@ import React from 'react'; import { Section, CompactTable } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { RBACResource, RBACUserRole } from '../rbac-types.ts'; -import { HEALTH_COLORS } from './utils.ts'; import { ConfigTypeIcon } from './configTypeIcon.tsx'; +const ROLE_SOURCE_COLORS: Record = { + direct: { bg: '#DBEAFE', fg: '#1E40AF' }, + group: { bg: '#F3E8FF', fg: '#6B21A8' }, +}; + +const CHANGELOG_TYPE_COLORS: Record = { + PermissionGranted: { bg: '#DCFCE7', fg: '#166534' }, + PermissionRevoked: { bg: '#FEE2E2', fg: '#991B1B' }, + AccessReviewed: { bg: '#DBEAFE', fg: '#1E40AF' }, +}; + interface Props { resource: RBACResource; } @@ -34,15 +44,26 @@ function age(iso?: string | null): string { return `${Math.floor(days / 365)}y ${Math.floor((days % 365) / 30)}mo`; } -function roleColumn(u: RBACUserRole): string { - const parts = [u.role]; - if (u.roleSource && u.roleSource !== 'direct') { - parts.push(`via ${u.roleSource}`); - } - if (u.sourceSystem && u.sourceSystem !== u.roleSource) { - parts.push(`(${u.sourceSystem})`); - } - return parts.join(' '); +function RoleSourceBadge({ source }: { source: string }) { + const key = source.startsWith('group:') ? 'group' : source; + const colors = ROLE_SOURCE_COLORS[key] || ROLE_SOURCE_COLORS.direct; + return ( + + {source} + + ); +} + +function roleColumn(u: RBACUserRole): React.ReactNode { + return ( + + {u.role} + + + ); } function ReviewAge({ u }: { u: RBACUserRole }) { @@ -58,7 +79,7 @@ function ReviewAge({ u }: { u: RBACUserRole }) { function LabelBadge({ label, value }: { label: string; value: string }) { return ( - + {label} @@ -72,7 +93,7 @@ function LabelBadge({ label, value }: { label: string; value: string }) { function Pill({ label, color, icon }: { label: string; color?: string; icon?: React.ReactNode }) { return ( +
{icon} {children}
@@ -119,13 +140,12 @@ function ResourceMeta({ resource }: Props) { if (resource.createdAt) dateParts.push(`Created: ${fmtDate(resource.createdAt)}`); if (resource.updatedAt) dateParts.push(`Updated: ${fmtDate(resource.updatedAt)}`); - const hasPills = resource.status || resource.health; const hasTags = (resource.tags && Object.keys(resource.tags).length > 0) || (resource.labels && Object.keys(resource.labels).length > 0); return (
-
+
ID: @@ -133,52 +153,56 @@ function ResourceMeta({ resource }: Props) { {dateParts.length > 0 && ( - + {dateParts.join(' \u2022 ')} )}
- {hasPills && ( + {resource.status && (
- {resource.health && ( - } - /> - )} - {resource.status && } +
)} {resource.description && ( -
{resource.description}
+
{resource.description}
)} {hasTags && }
); } -function ChangelogTable({ resource }: Props) { - if (!resource.changelog || resource.changelog.length === 0) return null; +function ChangeTypeBadge({ type }: { type: string }) { + const colors = CHANGELOG_TYPE_COLORS[type] || { bg: '#E2E8F0', fg: '#334155' }; + return ( + + {type} + + ); +} - const rows = resource.changelog.map((e) => [ - fmtDateTime(e.date), - e.changeType, - e.user, - e.role, - e.description, - ]); +function ChangelogList({ resource }: Props) { + if (!resource.changelog || resource.changelog.length === 0) return null; return (
}>Changelog - +
+ {resource.changelog.map((e, i) => ( +
+ {fmtDateTime(e.date)} + + {e.user} + + {e.role} + {e.description && {e.description}} +
+ ))} +
); } @@ -199,6 +223,7 @@ function TemporaryAccessTable({ resource }: Props) {
}>Temporary Access (<72h) + Role Source: + {Object.entries(ROLE_SOURCE_COLORS).map(([key, colors]) => ( + + + {key} + + ))} + Changelog: + {Object.entries(CHANGELOG_TYPE_COLORS).map(([key, colors]) => ( + + + {key} + + ))} +
+ ); +} + export default function RBACResourceSection({ resource }: Props) { const rows = resource.users.map((u) => [ u.userName, @@ -233,12 +279,14 @@ export default function RBACResourceSection({ resource }: Props) { }>Users - + + ); } diff --git a/report/components/RBACUserSection.tsx b/report/components/RBACUserSection.tsx index b24262835..15e1b2489 100644 --- a/report/components/RBACUserSection.tsx +++ b/report/components/RBACUserSection.tsx @@ -4,6 +4,11 @@ import { Icon } from '@flanksource/icons/icon'; import type { RBACUserReport, RBACUserResource } from '../rbac-types.ts'; import { ConfigTypeIcon } from './configTypeIcon.tsx'; +const ROLE_SOURCE_COLORS: Record = { + direct: { bg: '#DBEAFE', fg: '#1E40AF' }, + group: { bg: '#F3E8FF', fg: '#6B21A8' }, +}; + function fmtDate(iso: string): string { const d = new Date(iso); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; @@ -29,12 +34,26 @@ function ReviewAge({ r }: { r: RBACUserResource }) { return <>{text}; } -function roleColumn(r: RBACUserResource): string { - const parts = [r.role]; - if (r.roleSource && r.roleSource !== 'direct') { - parts.push(`via ${r.roleSource}`); - } - return parts.join(' '); +function RoleSourceBadge({ source }: { source: string }) { + const key = source.startsWith('group:') ? 'group' : source; + const colors = ROLE_SOURCE_COLORS[key] || ROLE_SOURCE_COLORS.direct; + return ( + + {source} + + ); +} + +function roleColumn(r: RBACUserResource): React.ReactNode { + return ( + + {r.role} + + + ); } interface Props { @@ -59,7 +78,7 @@ export default function RBACUserSection({ user }: Props) { {user.userName} - + ({user.email}) @@ -67,10 +86,10 @@ export default function RBACUserSection({ user }: Props) { return (
-
+
{title}
-
+
Source: {user.sourceSystem} @@ -97,7 +116,7 @@ export default function RBACUserSection({ user }: Props) { ]); return (
-
+
{configType} @@ -105,6 +124,7 @@ export default function RBACUserSection({ user }: Props) {
=7.3.2" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "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==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -39,6 +59,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -55,6 +76,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -71,6 +93,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -87,6 +110,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -103,6 +127,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -119,6 +144,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -135,6 +161,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -151,6 +178,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -167,6 +195,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -183,6 +212,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -199,6 +229,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -215,6 +246,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -231,6 +263,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -247,6 +280,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -263,6 +297,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -279,6 +314,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -295,6 +331,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -311,6 +348,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -327,6 +365,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -343,6 +382,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -359,6 +399,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -375,6 +416,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -391,6 +433,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -407,6 +450,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -423,6 +467,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -439,6 +484,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -449,33 +495,29 @@ } }, "node_modules/@flanksource/facet": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@flanksource/facet/-/facet-0.1.10.tgz", - "integrity": "sha512-5bIjOjxWo++YF8/Cq6XTlPSBe8M4/IdJqYszJ1ejlkFIy+kmyUFgYFv3z/s862Xi20kuMg2lDixcX+OsCYR6KQ==", + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@flanksource/facet/-/facet-0.1.30.tgz", + "integrity": "sha512-9BvGI70uv65Xen6Kh5SXfzMLDGEe3yjLutrVpkldSAJWEhGcz7IF7oveQ+MrWmAAUlQwanzJ45aNsBgSAsQvjQ==", "dependencies": { "@flanksource/icons": "^1.0.41", "@iconify/react": "^5.1.0", - "@tailwindcss/postcss": "^4.1.17", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", + "@testing-library/react": "^16.3.2", "@xyflow/react": "^12.0.0", - "chalk": "^5.6.2", "clsx": "^2.1.1", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "dagre": "^0.8.5", "dayjs": "^1.11.13", - "ora": "^8.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "pdf-lib": "^1.17.1", "react-icons": "^5.4.0", "shiki": "^1.0.0" }, - "bin": { - "facet": "cli/dist/cli.mjs" - }, "engines": { "node": ">=20.19" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/@flanksource/icons": { @@ -508,808 +550,195 @@ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "license": "MIT" }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "pako": "^1.0.6" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "pako": "^1.0.10" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true + "dependencies": { + "@shikijs/types": "1.29.2" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true + "dependencies": { + "@shikijs/types": "1.29.2" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "dependencies": { + "@types/d3-selection": "*" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "dependencies": { + "@types/d3-color": "*" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true + "dependencies": { + "@types/d3-selection": "*" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@shikijs/core": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", - "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", - "license": "MIT", - "dependencies": { - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.4" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", - "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "oniguruma-to-es": "^2.2.0" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", - "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1" - } - }, - "node_modules/@shikijs/langs": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", - "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2" - } - }, - "node_modules/@shikijs/themes": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", - "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2" - } - }, - "node_modules/@shikijs/types": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", - "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.31.1", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.1" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-x64": "4.2.1", - "@tailwindcss/oxide-freebsd-x64": "4.2.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", - "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", - "postcss": "^8.5.6", - "tailwindcss": "4.2.1" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", - "tailwindcss": "4.2.1" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" - } - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT", - "peer": true - }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1339,7 +768,7 @@ "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1390,15 +819,24 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { @@ -1407,6 +845,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1417,18 +864,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", @@ -1442,46 +877,19 @@ "node_modules/character-entities-legacy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1501,18 +909,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -1616,6 +1012,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -1725,15 +1122,6 @@ "node": ">=6" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -1747,10 +1135,10 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, "node_modules/emoji-regex-xs": { @@ -1759,23 +1147,11 @@ "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1808,471 +1184,126 @@ "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "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==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=12" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "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==", "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" + "argparse": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -2286,13 +1317,13 @@ "loose-envify": "cli.js" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "bin": { + "lz-string": "bin/bin.js" } }, "node_modules/mdast-util-to-hast": { @@ -2405,51 +1436,6 @@ ], "license": "MIT" }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/oniguruma-to-es": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", @@ -2461,27 +1447,22 @@ "regex-recursion": "^5.1.1" } }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" } }, "node_modules/picocolors": { @@ -2523,25 +1504,12 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": ">=4" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/property-information": { @@ -2559,6 +1527,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2571,6 +1540,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2588,6 +1558,12 @@ "react": "*" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, "node_modules/regex": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", @@ -2617,73 +1593,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" - } - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2709,27 +1624,6 @@ "@types/hast": "^3.0.4" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -2740,35 +1634,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -2844,11 +1709,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -2882,7 +1753,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unist-util-is": { @@ -2962,12 +1833,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/report/package.json b/report/package.json index 3dbddc505..d41e48179 100644 --- a/report/package.json +++ b/report/package.json @@ -9,7 +9,7 @@ "mission-control": "npm run pdf" }, "dependencies": { - "@flanksource/facet": "^0.1.10", + "@flanksource/facet": "^0.1.30", "@flanksource/icons": "^1.0.53", "js-yaml": "^4.1.0" }, From c6e2baab71ab4dc0b98293da5edd703ae9af6259 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 31 Mar 2026 06:30:15 +0300 Subject: [PATCH 21/48] feat(db): add access and access logs query functions for ui Add GetAccessForUIRef and GetAccessLogsForUIRef functions to query RBAC access data with filtering. Refactor GetRBACAccess to use ResourceSelector directly with optional recursive config expansion, replacing inline selector application. --- db/applications.go | 200 +++++++++++++++++++++++++++++++++++++++++++++ db/rbac.go | 54 +++++++----- 2 files changed, 233 insertions(+), 21 deletions(-) diff --git a/db/applications.go b/db/applications.go index f22106d16..1c4fe4e22 100644 --- a/db/applications.go +++ b/db/applications.go @@ -12,6 +12,7 @@ import ( "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" "github.com/flanksource/duty/query" + "github.com/flanksource/duty/types" "github.com/google/uuid" "github.com/samber/lo" "go.opentelemetry.io/otel/trace" @@ -519,3 +520,202 @@ func parseIncludeExcludeList(s string) (included, excluded []string) { } return } + +func GetAccessForUIRef(ctx context.Context, filters *api.AccessUIFilters) ([]api.AccessItem, error) { + if filters == nil { + filters = &api.AccessUIFilters{} + } + + q := ctx.DB(). + Table("config_access_summary"). + Select(`config_access_summary.config_id, + config_access_summary.config_name, + config_access_summary.config_type, + config_access_summary.external_user_id, + config_access_summary."user", + config_access_summary.email, + config_access_summary.role, + config_access_summary.user_type, + config_access_summary.created_at, + config_access_summary.last_signed_in_at, + config_access_summary.last_reviewed_at`). + Order(`config_access_summary.config_name, config_access_summary."user"`) + + if filters.Search != "" { + configIDs, err := query.FindConfigIDsByResourceSelector(ctx, 0, types.ResourceSelector{Search: filters.Search}) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to resolve config selector") + } + if len(configIDs) == 0 { + return nil, nil + } + q = q.Where("config_access_summary.config_id IN (?)", configIDs) + } + + if filters.ConfigTypes != "" { + included, excluded := parseIncludeExcludeList(filters.ConfigTypes) + if len(included) > 0 { + q = q.Where("config_access_summary.config_type IN (?)", included) + } + if len(excluded) > 0 { + q = q.Where("config_access_summary.config_type NOT IN (?)", excluded) + } + } + + if filters.Role != "" { + included, excluded := parseIncludeExcludeList(filters.Role) + if len(included) > 0 { + q = q.Where("config_access_summary.role IN (?)", included) + } + if len(excluded) > 0 { + q = q.Where("config_access_summary.role NOT IN (?)", excluded) + } + } + + if filters.UserType != "" { + included, excluded := parseIncludeExcludeList(filters.UserType) + if len(included) > 0 { + q = q.Where("config_access_summary.user_type IN (?)", included) + } + if len(excluded) > 0 { + q = q.Where("config_access_summary.user_type NOT IN (?)", excluded) + } + } + + var rows []RBACAccessRow + if err := q.Find(&rows).Error; err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to query access") + } + + var staleCutoff time.Time + if filters.Stale != "" { + if d, err := time.ParseDuration(filters.Stale); err == nil { + staleCutoff = time.Now().Add(-d) + } + } + + items := make([]api.AccessItem, 0, len(rows)) + for _, r := range rows { + isStale := !staleCutoff.IsZero() && (r.LastSignedInAt == nil || r.LastSignedInAt.Before(staleCutoff)) + if !staleCutoff.IsZero() && !isStale { + continue + } + items = append(items, api.AccessItem{ + ConfigID: r.ConfigID.String(), + ConfigName: r.ConfigName, + ConfigType: r.ConfigType, + UserID: r.UserID.String(), + UserName: r.UserName, + Email: r.Email, + Role: r.Role, + UserType: r.UserType, + CreatedAt: r.CreatedAt, + LastSignedInAt: r.LastSignedInAt, + LastReviewedAt: r.LastReviewedAt, + IsStale: isStale, + }) + } + + return items, nil +} + +func GetAccessLogsForUIRef(ctx context.Context, filters *api.AccessLogsUIFilters) ([]api.AccessLogItem, error) { + if filters == nil { + filters = &api.AccessLogsUIFilters{} + } + + q := ctx.DB(). + Table("config_access_logs"). + Select(`config_access_logs.config_id, + config_items.name AS config_name, + config_items.type AS config_type, + config_access_logs.external_user_id, + external_users.name AS user_name, + config_access_logs.created_at, + config_access_logs.mfa, + config_access_logs.count, + config_access_logs.properties`). + Joins("JOIN config_items ON config_items.id = config_access_logs.config_id"). + Joins("JOIN external_users ON external_users.id = config_access_logs.external_user_id"). + Order("config_access_logs.created_at DESC") + + if filters.Search != "" { + configIDs, err := query.FindConfigIDsByResourceSelector(ctx, 0, types.ResourceSelector{Search: filters.Search}) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to resolve config selector") + } + if len(configIDs) == 0 { + return nil, nil + } + q = q.Where("config_access_logs.config_id IN (?)", configIDs) + } + + if filters.ConfigTypes != "" { + included, excluded := parseIncludeExcludeList(filters.ConfigTypes) + if len(included) > 0 { + q = q.Where("config_items.type IN (?)", included) + } + if len(excluded) > 0 { + q = q.Where("config_items.type NOT IN (?)", excluded) + } + } + + if filters.From != "" { + if d, err := time.ParseDuration(filters.From); err == nil { + q = q.Where("config_access_logs.created_at >= ?", time.Now().Add(-d)) + } + } + + if filters.To != "" { + if d, err := time.ParseDuration(filters.To); err == nil { + q = q.Where("config_access_logs.created_at <= ?", time.Now().Add(-d)) + } + } + + if filters.MFA == "true" { + q = q.Where("config_access_logs.mfa = true") + } else if filters.MFA == "false" { + q = q.Where("config_access_logs.mfa = false") + } + + type accessLogRow struct { + ConfigID uuid.UUID `gorm:"column:config_id"` + ConfigName string `gorm:"column:config_name"` + ConfigType string `gorm:"column:config_type"` + ExternalUserID uuid.UUID `gorm:"column:external_user_id"` + UserName string `gorm:"column:user_name"` + CreatedAt time.Time `gorm:"column:created_at"` + MFA bool `gorm:"column:mfa"` + Count *int `gorm:"column:count"` + Properties types.JSONMap `gorm:"column:properties"` + } + + var rows []accessLogRow + if err := q.Scan(&rows).Error; err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to query access logs") + } + + items := make([]api.AccessLogItem, len(rows)) + for i, r := range rows { + var props map[string]string + if r.Properties != nil { + props = make(map[string]string, len(r.Properties)) + for k, v := range r.Properties { + props[k] = fmt.Sprintf("%v", v) + } + } + items[i] = api.AccessLogItem{ + ConfigID: r.ConfigID.String(), + ConfigName: r.ConfigName, + ConfigType: r.ConfigType, + UserID: r.ExternalUserID.String(), + UserName: r.UserName, + CreatedAt: r.CreatedAt, + MFA: r.MFA, + Count: lo.FromPtr(r.Count), + Properties: props, + } + } + + return items, nil +} diff --git a/db/rbac.go b/db/rbac.go index a1827b7f2..f82d259fb 100644 --- a/db/rbac.go +++ b/db/rbac.go @@ -5,9 +5,10 @@ import ( "time" "github.com/flanksource/duty/context" + dutyQuery "github.com/flanksource/duty/query" "github.com/flanksource/duty/types" "github.com/google/uuid" - "gorm.io/gorm" + "github.com/samber/lo" "github.com/flanksource/incident-commander/api" ) @@ -34,8 +35,8 @@ func (r RBACAccessRow) RoleSource() string { return "direct" } -func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector) ([]RBACAccessRow, error) { - query := ctx.DB(). +func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector, recursive bool) ([]RBACAccessRow, error) { + q := ctx.DB(). Table("config_access_summary"). Select(`config_access_summary.config_id, config_access_summary.config_name, @@ -51,10 +52,25 @@ func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector) ([]R config_access_summary.last_reviewed_at`). Joins("LEFT JOIN external_groups ON config_access_summary.external_group_id = external_groups.id") - query = applyAccessSelectors(query, selectors) + if len(selectors) > 0 { + configIDs, err := dutyQuery.FindConfigIDsByResourceSelector(ctx, 0, selectors...) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to resolve config selectors") + } + if len(configIDs) == 0 { + return nil, nil + } + if recursive { + configIDs, err = expandConfigChildren(ctx, configIDs) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to expand children") + } + } + q = q.Where("config_access_summary.config_id IN (?)", configIDs) + } var rows []RBACAccessRow - if err := query. + if err := q. Order("config_access_summary.config_name, config_access_summary.\"user\""). Find(&rows).Error; err != nil { return nil, ctx.Oops().Wrapf(err, "failed to query RBAC access") @@ -63,28 +79,24 @@ func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector) ([]R return rows, nil } -func applyAccessSelectors(query *gorm.DB, selectors []types.ResourceSelector) *gorm.DB { - if len(selectors) == 0 { - return query +func expandConfigChildren(ctx context.Context, ids []uuid.UUID) ([]uuid.UUID, error) { + allIDs := make(map[uuid.UUID]struct{}, len(ids)) + for _, id := range ids { + allIDs[id] = struct{}{} } - for _, s := range selectors { - if s.ID != "" { - query = query.Where("config_access_summary.config_id = ?", s.ID) - } - if len(s.Types) > 0 { - query = query.Where("config_access_summary.config_type IN (?)", s.Types) - } - if s.Name != "" { - query = query.Where("config_access_summary.config_name ILIKE ?", s.Name) + for _, id := range ids { + var children []uuid.UUID + if err := ctx.DB().Raw("SELECT child_id FROM lookup_config_children(?, -1)", id.String()). + Scan(&children).Error; err != nil { + return nil, err } - if s.Search != "" { - pattern := "%" + s.Search + "%" - query = query.Where("(config_access_summary.config_name ILIKE ? OR config_access_summary.config_type ILIKE ?)", pattern, pattern) + for _, child := range children { + allIDs[child] = struct{}{} } } - return query + return lo.Keys(allIDs), nil } func GetRBACChangelog(ctx context.Context, configIDs []uuid.UUID, since time.Time) ([]api.RBACChangeEntry, error) { From 92e5bceff92f09e1a3ae1a40b2d9813dc05af2d7 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 31 Mar 2026 06:30:53 +0300 Subject: [PATCH 22/48] feat(api): add catalog get command for detailed config item inspection Introduces new `catalog get` command that displays comprehensive config item details including relationships, insights, changes, access logs, and playbook runs. Also adds --report-source flag across export commands to support local TSX report overrides. --- cmd/application.go | 2 + cmd/catalog.go | 66 +----- cmd/catalog_get.go | 458 ++++++++++++++++++++++++++++++++++++++++ cmd/catalog_get_test.go | 249 ++++++++++++++++++++++ cmd/rbac.go | 2 + cmd/view.go | 2 + 6 files changed, 724 insertions(+), 55 deletions(-) create mode 100644 cmd/catalog_get.go create mode 100644 cmd/catalog_get_test.go diff --git a/cmd/application.go b/cmd/application.go index 4cbe6909e..e7d59bb6f 100644 --- a/cmd/application.go +++ b/cmd/application.go @@ -17,6 +17,7 @@ import ( v1 "github.com/flanksource/incident-commander/api/v1" "github.com/flanksource/incident-commander/application" "github.com/flanksource/incident-commander/db" + "github.com/flanksource/incident-commander/report" ) var ApplicationCmd = &cobra.Command{ @@ -90,5 +91,6 @@ var ExportApplication = &cobra.Command{ func init() { ExportApplication.Flags().StringVarP(&exportFormat, "format", "f", "json", "Output format: json, html, pdf, facet-html, facet-pdf") ExportApplication.Flags().StringVarP(&exportOutfile, "out-file", "o", "", "Write output to file instead of stdout") + ExportApplication.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory with TSX report files (overrides embedded reports)") ApplicationCmd.AddCommand(ExportApplication) } diff --git a/cmd/catalog.go b/cmd/catalog.go index cb6fc587d..6460f1756 100644 --- a/cmd/catalog.go +++ b/cmd/catalog.go @@ -1,9 +1,7 @@ package cmd import ( - "fmt" "os" - "strconv" "strings" "time" @@ -26,53 +24,10 @@ var Catalog = &cobra.Command{ func parseQuery(args []string) query.SearchResourcesRequest { logger.Infof("Search query %v", args) - request := query.SearchResourcesRequest{ - Limit: 5, + return query.SearchResourcesRequest{ + Limit: 100, + Configs: []types.ResourceSelector{{Cache: "no-cache", Search: strings.Join(args, " ")}}, } - tags := make(map[string]string) - selector := types.ResourceSelector{ - Cache: "no-cache", - } - for _, arg := range args { - parts := strings.Split(arg, "=") - if len(parts) != 2 { - logger.Warnf("Invalid param: %s", arg) - continue - } - - switch parts[0] { - case "limit": - l, _ := strconv.Atoi(parts[1]) - request.Limit = l - case "search": - selector.Search = parts[1] - case "scope": - selector.Scope = parts[1] - case "type": - selector.Types = append(selector.Types, parts[1]) - case "name": - selector.Name = parts[1] - case "namespace": - selector.Namespace = parts[1] - case "id": - selector.ID = parts[1] - case "status": - selector.Statuses = append(selector.Statuses, parts[1]) - default: - tags[parts[0]] = parts[1] - } - } - - for k, v := range tags { - if strings.HasPrefix(k, "@") { - selector.TagSelector += fmt.Sprintf(" %s=%s", k[1:], v) - } else { - selector.LabelSelector += fmt.Sprintf(" %s=%s", k, v) - } - } - request.Configs = []types.ResourceSelector{selector} - - return request } var Query = &cobra.Command{ @@ -123,17 +78,12 @@ var Query = &cobra.Command{ if catalogOutfile != "" { logger.Infof("Writing output to %s", catalogOutfile) - if err := clicky.FormatToFile(*response, clicky.FormatOptions{}, catalogOutfile); err != nil { + if err := clicky.FormatToFile(*response, clicky.Flags.FormatOptions, catalogOutfile); err != nil { logger.Fatalf(err.Error()) os.Exit(1) } } else { - out, err := clicky.Format(*response) - if err != nil { - logger.Fatalf(err.Error()) - os.Exit(1) - } - fmt.Println(out) + clicky.MustPrint(*response, clicky.Flags.FormatOptions) } }, } @@ -176,7 +126,13 @@ var Mock = &cobra.Command{ func init() { Query.Flags().StringVarP(&catalogOutfile, "out-file", "o", "", "Write catalog output to a file instead of stdout") Query.Flags().DurationVarP(&catalogWaitFor, "wait", "w", 60*time.Second, "Wait for this long for resources to be discovered") + clicky.BindAllFlags(Query.PersistentFlags(), "format") + + Get.Flags().DurationVar(&catalogGetSince, "since", 7*24*time.Hour, "Show changes and playbook runs since this duration ago") + clicky.BindAllFlags(Get.PersistentFlags(), "format") + Catalog.AddCommand(Query) Catalog.AddCommand(Mock) + Catalog.AddCommand(Get) Root.AddCommand(Catalog) } diff --git a/cmd/catalog_get.go b/cmd/catalog_get.go new file mode 100644 index 000000000..b69de6700 --- /dev/null +++ b/cmd/catalog_get.go @@ -0,0 +1,458 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/flanksource/clicky" + "github.com/flanksource/clicky/api" + "github.com/flanksource/commons/logger" + "github.com/flanksource/commons/properties" + "github.com/flanksource/duty" + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query" + "github.com/google/uuid" + "github.com/samber/lo" + "github.com/spf13/cobra" +) + +var catalogGetSince time.Duration + +var Get = &cobra.Command{ + Use: "get ", + Short: "Get a full config item with relationships, insights, changes, access, and playbook runs", + Args: cobra.MinimumNArgs(1), + PersistentPreRun: PreRun, + RunE: func(cmd *cobra.Command, args []string) error { + logger.UseSlog() + if err := properties.LoadFile("mission-control.properties"); err != nil { + logger.Errorf(err.Error()) + } + ctx, stop, err := duty.Start("mission-control", duty.ClientOnly) + if err != nil { + return err + } + defer stop() + + result, err := runCatalogGet(ctx, args, catalogGetSince) + if err != nil { + return err + } + + clicky.MustPrint(result, clicky.Flags.FormatOptions) + return nil + }, +} + +func resolveConfigID(ctx context.Context, args []string) (*models.ConfigItem, error) { + if id, err := uuid.Parse(args[0]); err == nil { + config, err := query.GetCachedConfig(ctx, id.String()) + if err != nil { + return nil, fmt.Errorf("failed to get config %s: %w", id, err) + } + if config == nil { + return nil, fmt.Errorf("config item %s not found", id) + } + return config, nil + } + + req := parseQuery(args) + req.Limit = 2 + response, err := query.SearchResources(ctx, req) + if err != nil { + return nil, fmt.Errorf("search failed: %w", err) + } + if len(response.Configs) == 0 { + return nil, fmt.Errorf("no config found matching query") + } + if len(response.Configs) > 1 { + return nil, fmt.Errorf("query matched multiple configs, expected exactly one") + } + + config, err := query.GetCachedConfig(ctx, response.Configs[0].ID) + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + if config == nil { + return nil, fmt.Errorf("config item not found") + } + return config, nil +} + +type CatalogGetResult struct { + models.ConfigItem `json:",inline"` + LastScrapedTime *time.Time `json:"last_scraped_time,omitempty"` + Related []query.RelatedConfig `json:"related,omitempty"` + Insights []models.ConfigAnalysis `json:"insights,omitempty"` + Changes []models.ConfigChange `json:"changes,omitempty"` + Access []models.ConfigAccessSummary `json:"access,omitempty"` + PlaybookRuns []models.PlaybookRun `json:"playbook_runs,omitempty"` + + since time.Duration +} + +func runCatalogGet(ctx context.Context, args []string, since time.Duration) (*CatalogGetResult, error) { + config, err := resolveConfigID(ctx, args) + if err != nil { + return nil, err + } + + sinceTime := time.Now().Add(-since) + id := config.ID + + result := &CatalogGetResult{ConfigItem: *config, since: since} + + var lastScraped models.ConfigItemLastScrapedTime + if err := ctx.DB().Where("config_id = ?", id).First(&lastScraped).Error; err == nil { + result.LastScrapedTime = lastScraped.LastScrapedTime + } + + result.Related, err = query.GetRelatedConfigs(ctx, query.RelationQuery{ID: id}) + if err != nil { + return nil, fmt.Errorf("failed to get related configs: %w", err) + } + + if err := ctx.DB().Where("config_id = ? AND status = 'open'", id). + Find(&result.Insights).Error; err != nil { + return nil, fmt.Errorf("failed to get insights: %w", err) + } + + if err := ctx.DB().Where("config_id = ? AND created_at >= ?", id, sinceTime). + Order("created_at DESC").Limit(50). + Find(&result.Changes).Error; err != nil { + return nil, fmt.Errorf("failed to get changes: %w", err) + } + + result.Access, err = query.FindConfigAccessByConfigIDs(ctx, []uuid.UUID{id}) + if err != nil { + return nil, fmt.Errorf("failed to get access: %w", err) + } + + if err := ctx.DB().Where("config_id = ? AND created_at >= ?", id, sinceTime). + Order("created_at DESC").Limit(50). + Find(&result.PlaybookRuns).Error; err != nil { + return nil, fmt.Errorf("failed to get playbook runs: %w", err) + } + + return result, nil +} + +func (r CatalogGetResult) Pretty() api.Text { + t := r.ConfigItem.Pretty() + t = t.NewLine().Append(buildDetailsSection(r)) + + if r.ConfigItem.Config != nil && *r.ConfigItem.Config != "" { + t = t.NewLine().Append(clicky.Collapsed("Config", configCodeBlock(*r.ConfigItem.Config))) + } + + if len(r.Related) > 0 { + tree := buildRelationshipTree(&r.ConfigItem, r.Related) + t = t.NewLine().Append(clicky.Collapsed( + fmt.Sprintf("Relationships (%d)", len(r.Related)), + tree, + )) + } + + sinceLabel := formatDuration(r.since) + + if len(r.Insights) > 0 { + rows := lo.Map(r.Insights, func(a models.ConfigAnalysis, _ int) analysisRow { + return analysisRow{a} + }) + t = t.NewLine().Append(clicky.Collapsed( + fmt.Sprintf("Open Insights (%d)", len(rows)), + api.NewTableFrom(rows), + )) + } + + if len(r.Changes) > 0 { + t = t.NewLine().Append(clicky.Collapsed( + fmt.Sprintf("Changes since %s (%d)", sinceLabel, len(r.Changes)), + api.NewTableFrom(r.Changes), + )) + } + + if len(r.Access) > 0 { + rows := lo.Map(r.Access, func(a models.ConfigAccessSummary, _ int) accessRow { + return accessRow{a} + }) + t = t.NewLine().Append(clicky.Collapsed( + fmt.Sprintf("Access (%d)", len(rows)), + api.NewTableFrom(rows), + )) + } + + if len(r.PlaybookRuns) > 0 { + rows := lo.Map(r.PlaybookRuns, func(p models.PlaybookRun, _ int) playbookRunRow { + return playbookRunRow{p} + }) + t = t.NewLine().Append(clicky.Collapsed( + fmt.Sprintf("Playbook Runs since %s (%d)", sinceLabel, len(rows)), + api.NewTableFrom(rows), + )) + } + + return t +} + +func buildDetailsSection(r CatalogGetResult) api.DescriptionList { + c := &r.ConfigItem + items := []api.KeyValuePair{ + {Key: "ID", Value: c.ID.String()}, + {Key: "Type", Value: lo.FromPtrOr(c.Type, "-")}, + {Key: "Class", Value: c.ConfigClass}, + } + + if c.Health != nil { + items = append(items, api.KeyValuePair{Key: "Health", Value: c.Health.Pretty()}) + } + if c.Status != nil { + items = append(items, api.KeyValuePair{Key: "Status", Value: *c.Status}) + } + if c.Description != nil && *c.Description != "" { + items = append(items, api.KeyValuePair{Key: "Description", Value: *c.Description}) + } + if c.Source != nil && *c.Source != "" { + items = append(items, api.KeyValuePair{Key: "Source", Value: *c.Source}) + } + if c.ScraperID != nil && *c.ScraperID != "" { + items = append(items, api.KeyValuePair{Key: "Scraper", Value: *c.ScraperID}) + } + if r.LastScrapedTime != nil { + items = append(items, api.KeyValuePair{Key: "Last Scraped", Value: api.Human(time.Since(*r.LastScrapedTime), "text-gray-600")}) + } + if c.AgentID != uuid.Nil { + items = append(items, api.KeyValuePair{Key: "Agent", Value: c.AgentID.String()}) + } + if c.Ready { + items = append(items, api.KeyValuePair{Key: "Ready", Value: "true"}) + } + if c.Path != "" { + items = append(items, api.KeyValuePair{Key: "Path", Value: c.Path}) + } + if c.ParentID != nil { + items = append(items, api.KeyValuePair{Key: "Parent", Value: c.ParentID.String()}) + } + if len(c.ExternalID) > 0 { + items = append(items, api.KeyValuePair{Key: "External ID", Value: c.ExternalID[0]}) + } + + if c.CostTotal30d > 0 { + items = append(items, api.KeyValuePair{Key: "Cost (30d)", Value: fmt.Sprintf("$%.2f", c.CostTotal30d)}) + } + + if !c.CreatedAt.IsZero() { + items = append(items, api.KeyValuePair{Key: "Created", Value: api.Human(time.Since(c.CreatedAt), "text-gray-600")}) + } + if c.UpdatedAt != nil { + items = append(items, api.KeyValuePair{Key: "Updated", Value: api.Human(time.Since(*c.UpdatedAt), "text-gray-600")}) + } + if c.DeletedAt != nil { + items = append(items, api.KeyValuePair{Key: "Deleted", Value: api.Human(time.Since(*c.DeletedAt), "text-red-600")}) + if c.DeleteReason != "" { + items = append(items, api.KeyValuePair{Key: "Delete Reason", Value: c.DeleteReason}) + } + } + + if c.Labels != nil && len(*c.Labels) > 0 { + items = append(items, api.KeyValuePair{Key: "Labels", Value: clicky.Map(*c.Labels, "text-xs")}) + } + if len(c.Tags) > 0 { + items = append(items, api.KeyValuePair{Key: "Tags", Value: clicky.Map(c.Tags, "text-xs")}) + } + + if c.Properties != nil { + for _, p := range *c.Properties { + val := p.Text + if val == "" && p.Value != nil { + val = fmt.Sprintf("%d", *p.Value) + } + if val == "" { + continue + } + label := p.Label + if label == "" { + label = p.Name + } + items = append(items, api.KeyValuePair{Key: label, Value: val}) + } + } + + return api.DescriptionList{Items: items} +} + +func configCodeBlock(configJSON string) api.Code { + var parsed any + if err := json.Unmarshal([]byte(configJSON), &parsed); err == nil { + if pretty, err := json.MarshalIndent(parsed, "", " "); err == nil { + configJSON = string(pretty) + } + } + return api.CodeBlock("json", configJSON) +} + +// relatedConfigNode implements api.TreeNode for relationship tree rendering. +type relatedConfigNode struct { + label api.Text + children []api.TreeNode +} + +func (n relatedConfigNode) Pretty() api.Text { return n.label } +func (n relatedConfigNode) GetChildren() []api.TreeNode { return n.children } + +func buildRelationshipTree(config *models.ConfigItem, related []query.RelatedConfig) api.TextTree { + // Index all nodes by ID + nodes := make(map[string]*relatedConfigNode) + rootID := config.ID.String() + nodes[rootID] = &relatedConfigNode{label: config.Pretty()} + + for _, rc := range related { + nodes[rc.ID.String()] = &relatedConfigNode{label: relatedConfigLabel(rc)} + } + + // Build parent-child edges from Path (format: "grandparent.parent.child") + for _, rc := range related { + parentID := parentIDFromPath(rc.Path, rc.ID.String()) + if parentID == "" { + parentID = rootID + } + if parent, ok := nodes[parentID]; ok { + parent.children = append(parent.children, nodes[rc.ID.String()]) + } else { + // parent not in result set, attach to root + nodes[rootID].children = append(nodes[rootID].children, nodes[rc.ID.String()]) + } + } + + return api.NewTree[api.TreeNode](nodes[rootID]) +} + +// parentIDFromPath extracts the parent ID from a dot-separated path. +// For path "a.b.c" and id "c", returns "b". +func parentIDFromPath(path, id string) string { + if path == "" { + return "" + } + segments := strings.Split(path, ".") + for i, seg := range segments { + if seg == id && i > 0 { + return segments[i-1] + } + } + return "" +} + +func relatedConfigLabel(rc query.RelatedConfig) api.Text { + t := clicky.Text("") + if rc.Health != nil { + t = t.Add(rc.Health.Pretty()).AddText(" ") + } + t = t.AddText(rc.Name, "font-bold") + t = t.AddText(" ").Add(clicky.Text(rc.Type, "text-xs text-gray-600").Wrap("(", ")")) + if rc.Status != nil && *rc.Status != "" { + t = t.AddText(" ").Add(clicky.Text(*rc.Status, "text-xs text-gray-500")) + } + return t +} + +func formatDuration(d time.Duration) string { + if d >= 24*time.Hour { + days := int(d.Hours() / 24) + return fmt.Sprintf("%dd", days) + } + return d.String() +} + +// analysisRow wraps ConfigAnalysis for TableProvider. +type analysisRow struct { + models.ConfigAnalysis +} + +func (r analysisRow) Columns() []api.ColumnDef { + return []api.ColumnDef{ + api.Column("Severity").Build(), + api.Column("Type").Build(), + api.Column("Analyzer").Build(), + api.Column("Summary").Build(), + api.Column("Status").Build(), + } +} + +func (r analysisRow) Row() map[string]any { + return map[string]any{ + "Severity": r.ConfigAnalysis.Severity.Pretty(), + "Type": r.ConfigAnalysis.AnalysisType.Pretty(), + "Analyzer": clicky.Text(r.Analyzer, "font-bold"), + "Summary": clicky.Text(r.Summary, "text-gray-700"), + "Status": clicky.Text(r.ConfigAnalysis.Status, "text-blue-600"), + } +} + +// accessRow wraps ConfigAccessSummary for TableProvider. +type accessRow struct { + models.ConfigAccessSummary +} + +func (r accessRow) Columns() []api.ColumnDef { + return []api.ColumnDef{ + api.Column("User").Build(), + api.Column("Role").Build(), + api.Column("Email").Build(), + api.Column("UserType").Label("Type").Build(), + api.Column("LastSignedIn").Label("Last Signed In").Build(), + } +} + +func (r accessRow) Row() map[string]any { + lastSignedIn := clicky.Text("-", "text-gray-400") + if r.LastSignedInAt != nil { + lastSignedIn = api.Human(time.Since(*r.LastSignedInAt), "text-gray-600") + } + return map[string]any{ + "User": clicky.Text(r.User, "font-bold"), + "Role": clicky.Text(r.Role), + "Email": clicky.Text(r.Email, "text-gray-600"), + "UserType": clicky.Text(r.UserType, "text-gray-500"), + "LastSignedIn": lastSignedIn, + } +} + +// playbookRunRow wraps PlaybookRun for TableProvider. +type playbookRunRow struct { + models.PlaybookRun +} + +func (r playbookRunRow) Columns() []api.ColumnDef { + return []api.ColumnDef{ + api.Column("Status").Build(), + api.Column("ID").Build(), + api.Column("Duration").Build(), + api.Column("CreatedAt").Label("Created").Build(), + api.Column("Error").Build(), + } +} + +func (r playbookRunRow) Row() map[string]any { + row := map[string]any{ + "Status": r.PlaybookRun.Status.Pretty(), + "ID": clicky.Text(r.PlaybookRun.ID.String()[:8], "font-mono text-xs"), + "Duration": clicky.Text("-", "text-gray-400"), + "CreatedAt": api.Human(time.Since(r.CreatedAt), "text-gray-600"), + "Error": clicky.Text(""), + } + + if r.StartTime != nil && r.EndTime != nil { + row["Duration"] = api.Human(r.EndTime.Sub(*r.StartTime), "text-gray-600") + } else if r.StartTime != nil { + row["Duration"] = api.Human(time.Since(*r.StartTime), "text-blue-600") + } + + if r.PlaybookRun.Error != nil && *r.PlaybookRun.Error != "" { + row["Error"] = clicky.Text(*r.PlaybookRun.Error, "text-red-600 text-sm") + } + + return row +} diff --git a/cmd/catalog_get_test.go b/cmd/catalog_get_test.go new file mode 100644 index 000000000..9202b0d50 --- /dev/null +++ b/cmd/catalog_get_test.go @@ -0,0 +1,249 @@ +package cmd + +import ( + "time" + + "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query" + "github.com/flanksource/duty/types" + "github.com/google/uuid" + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/samber/lo" +) + +var _ = ginkgo.Describe("buildCatalogGetOutput", func() { + configID := uuid.New() + since := 7 * 24 * time.Hour + + makeConfig := func() *models.ConfigItem { + return &models.ConfigItem{ + ID: configID, + Name: lo.ToPtr("my-deployment"), + Type: lo.ToPtr("Kubernetes::Deployment"), + ConfigClass: "Deployment", + Health: lo.ToPtr(models.HealthHealthy), + } + } + + ginkgo.It("renders all sections when data is present", func() { + configJSON := `{"replicas": 3}` + c := makeConfig() + c.Config = &configJSON + r := CatalogGetResult{ + ConfigItem: *c, + since: since, + Related: []query.RelatedConfig{ + {ID: uuid.New(), Name: "my-pod", Type: "Kubernetes::Pod", Relation: "outgoing", Health: lo.ToPtr(models.HealthHealthy)}, + }, + Insights: []models.ConfigAnalysis{ + {ID: uuid.New(), ConfigID: configID, Analyzer: "test-analyzer", AnalysisType: models.AnalysisTypeSecurity, Severity: models.SeverityHigh, Status: "open", Summary: "test finding"}, + }, + Changes: []models.ConfigChange{ + {ID: uuid.NewString(), ConfigID: configID.String(), ChangeType: "diff", Severity: models.SeverityInfo, Summary: "field changed", CreatedAt: lo.ToPtr(time.Now())}, + }, + Access: []models.ConfigAccessSummary{ + {ConfigID: configID, User: "alice", Role: "admin", Email: "alice@example.com", UserType: "user"}, + }, + PlaybookRuns: []models.PlaybookRun{ + {ID: uuid.New(), ConfigID: &configID, Status: models.PlaybookRunStatusCompleted, CreatedAt: time.Now()}, + }, + } + + out := r.Pretty().String() + Expect(out).To(ContainSubstring("my-deployment")) + Expect(out).To(ContainSubstring("Relationships")) + Expect(out).To(ContainSubstring("Open Insights")) + Expect(out).To(ContainSubstring("Changes since")) + Expect(out).To(ContainSubstring("Access")) + Expect(out).To(ContainSubstring("Playbook Runs")) + }) + + ginkgo.It("omits empty sections but always includes header and details", func() { + r := CatalogGetResult{ConfigItem: *makeConfig(), since: since} + out := r.Pretty().String() + Expect(out).To(ContainSubstring("my-deployment")) + Expect(out).NotTo(ContainSubstring("Relationships")) + Expect(out).NotTo(ContainSubstring("Open Insights")) + }) + + ginkgo.It("includes config code block when config JSON is present", func() { + configJSON := `{"foo":"bar"}` + c := makeConfig() + c.Config = &configJSON + r := CatalogGetResult{ConfigItem: *c, since: since} + out := r.Pretty().String() + Expect(out).To(ContainSubstring("Config")) + Expect(out).To(ContainSubstring("foo")) + }) +}) + +var _ = ginkgo.Describe("buildDetailsSection", func() { + ginkgo.It("includes scraper and last scraped time", func() { + scraperID := "scraper-123" + lastScraped := time.Now().Add(-10 * time.Minute) + r := CatalogGetResult{ + ConfigItem: models.ConfigItem{ + ID: uuid.New(), + Name: lo.ToPtr("test"), + Type: lo.ToPtr("Kubernetes::Pod"), + ScraperID: &scraperID, + }, + LastScrapedTime: &lastScraped, + } + dl := buildDetailsSection(r) + + var foundScraper, foundLastScraped bool + for _, item := range dl.Items { + if item.Key == "Scraper" { + foundScraper = true + Expect(item.Value).To(Equal("scraper-123")) + } + if item.Key == "Last Scraped" { + foundLastScraped = true + } + } + Expect(foundScraper).To(BeTrue()) + Expect(foundLastScraped).To(BeTrue()) + }) + + ginkgo.It("includes properties", func() { + val := int64(42) + r := CatalogGetResult{ + ConfigItem: models.ConfigItem{ + ID: uuid.New(), + Name: lo.ToPtr("test"), + Type: lo.ToPtr("AWS::EC2::Instance"), + Properties: &types.Properties{ + {Label: "Instance Type", Text: "t3.micro"}, + {Name: "cpu_count", Value: &val}, + }, + }, + } + dl := buildDetailsSection(r) + + var foundInstanceType, foundCPU bool + for _, item := range dl.Items { + if item.Key == "Instance Type" { + foundInstanceType = true + Expect(item.Value).To(Equal("t3.micro")) + } + if item.Key == "cpu_count" { + foundCPU = true + Expect(item.Value).To(Equal("42")) + } + } + Expect(foundInstanceType).To(BeTrue()) + Expect(foundCPU).To(BeTrue()) + }) +}) + +var _ = ginkgo.Describe("buildRelationshipTree", func() { + ginkgo.It("builds parent-child tree from paths", func() { + rootID := uuid.New() + childID := uuid.New() + grandchildID := uuid.New() + config := &models.ConfigItem{ + ID: rootID, + Name: lo.ToPtr("root"), + Type: lo.ToPtr("Kubernetes::Deployment"), + } + related := []query.RelatedConfig{ + {ID: childID, Name: "child", Type: "Kubernetes::Pod", Path: rootID.String() + "." + childID.String()}, + {ID: grandchildID, Name: "grandchild", Type: "Kubernetes::Container", Path: rootID.String() + "." + childID.String() + "." + grandchildID.String()}, + } + tree := buildRelationshipTree(config, related) + // root has 1 child, that child has 1 grandchild + Expect(tree.Children).To(HaveLen(1)) + Expect(tree.Children[0].Children).To(HaveLen(1)) + }) + + ginkgo.It("attaches orphans to root", func() { + rootID := uuid.New() + orphanID := uuid.New() + config := &models.ConfigItem{ + ID: rootID, + Name: lo.ToPtr("root"), + Type: lo.ToPtr("Kubernetes::Deployment"), + } + related := []query.RelatedConfig{ + {ID: orphanID, Name: "orphan", Type: "Kubernetes::Pod", Path: ""}, + } + tree := buildRelationshipTree(config, related) + Expect(tree.Children).To(HaveLen(1)) + }) +}) + +var _ = ginkgo.Describe("parentIDFromPath", func() { + for _, tt := range []struct { + name string + path string + id string + expected string + }{ + {"middle of path", "a.b.c", "c", "b"}, + {"root child", "a.b", "b", "a"}, + {"first element", "a.b", "a", ""}, + {"empty path", "", "a", ""}, + {"not in path", "a.b.c", "d", ""}, + } { + ginkgo.It(tt.name, func() { + Expect(parentIDFromPath(tt.path, tt.id)).To(Equal(tt.expected)) + }) + } +}) + +var _ = ginkgo.Describe("configCodeBlock", func() { + ginkgo.It("pretty prints JSON", func() { + code := configCodeBlock(`{"a":1,"b":"c"}`) + Expect(code.String()).To(ContainSubstring("\"a\": 1")) + }) +}) + +var _ = ginkgo.Describe("formatDuration", func() { + for _, tt := range []struct { + name string + input time.Duration + expected string + }{ + {"7 days", 7 * 24 * time.Hour, "7d"}, + {"1 day", 24 * time.Hour, "1d"}, + {"30 days", 30 * 24 * time.Hour, "30d"}, + {"sub-day", 6 * time.Hour, "6h0m0s"}, + } { + ginkgo.It(tt.name, func() { + Expect(formatDuration(tt.input)).To(Equal(tt.expected)) + }) + } +}) + +var _ = ginkgo.Describe("TableProvider wrappers", func() { + ginkgo.It("analysisRow returns correct columns", func() { + r := analysisRow{models.ConfigAnalysis{Analyzer: "test", Severity: models.SeverityHigh}} + Expect(r.Columns()).To(HaveLen(5)) + row := r.Row() + Expect(row).To(HaveKey("Severity")) + Expect(row).To(HaveKey("Analyzer")) + }) + + ginkgo.It("accessRow handles nil LastSignedInAt", func() { + r := accessRow{models.ConfigAccessSummary{User: "bob", Role: "viewer"}} + row := r.Row() + Expect(row).To(HaveKey("LastSignedIn")) + }) + + ginkgo.It("playbookRunRow computes duration", func() { + start := time.Now().Add(-5 * time.Minute) + end := time.Now() + r := playbookRunRow{models.PlaybookRun{ + ID: uuid.New(), + Status: models.PlaybookRunStatusCompleted, + CreatedAt: start, + StartTime: &start, + EndTime: &end, + }} + row := r.Row() + Expect(row).To(HaveKey("Duration")) + Expect(row).To(HaveKey("Status")) + }) +}) diff --git a/cmd/rbac.go b/cmd/rbac.go index 91eb407af..4d6ce6473 100644 --- a/cmd/rbac.go +++ b/cmd/rbac.go @@ -16,6 +16,7 @@ import ( v1 "github.com/flanksource/incident-commander/api/v1" "github.com/flanksource/incident-commander/rbac_report" + "github.com/flanksource/incident-commander/report" ) var RBACCmd = &cobra.Command{ @@ -165,5 +166,6 @@ func init() { ExportRBAC.Flags().StringVar(&rbacSince, "since", "2160h", "Changelog time range (Go duration, default 90 days)") ExportRBAC.Flags().StringVar(&rbacTitle, "title", "", "Report title (default auto-generated)") ExportRBAC.Flags().BoolVar(&rbacByUser, "by-user", false, "Group report by user instead of resource") + ExportRBAC.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory with TSX report files (overrides embedded reports)") RBACCmd.AddCommand(ExportRBAC) } diff --git a/cmd/view.go b/cmd/view.go index 2df506433..07a5d85a2 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -13,6 +13,7 @@ import ( yamlutil "k8s.io/apimachinery/pkg/util/yaml" v1 "github.com/flanksource/incident-commander/api/v1" + "github.com/flanksource/incident-commander/report" "github.com/flanksource/incident-commander/views" ) @@ -109,5 +110,6 @@ func init() { ViewRun.Flags().StringVarP(&viewFormat, "format", "f", "json", "Output format: json, csv, html, pdf, facet-html, facet-pdf") ViewRun.Flags().StringVarP(&viewOutFile, "out-file", "o", "", "Write output to file instead of stdout") ViewRun.Flags().StringSliceVar(&viewVars, "var", nil, "Template variables as key=value pairs") + ViewRun.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory with TSX report files (overrides embedded reports)") ViewCmd.AddCommand(ViewRun) } From 4cdf95ecdb2386a8fe2d62c2b3793309a1fe5d0b Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 31 Mar 2026 06:31:59 +0300 Subject: [PATCH 23/48] feat(api): add access and access logs sections to application views Introduces AccessItem, AccessLogItem, AccessUIFilters, and AccessLogsUIFilters types to support rendering access control and audit log sections in application UI. Refactors FacetOptions to use embedded FacetRenderOptions struct and adds PDF rendering support to ViewSpec. --- api/application.go | 50 +++++++-- api/v1/playbook_actions.go | 16 +-- api/v1/view_types.go | 4 +- api/v1/zz_generated.deepcopy.go | 32 ++++-- api/view.go | 24 ++++- api/zz_generated.deepcopy.go | 40 +++++++ application/application.go | 26 +++++ ...-control.flanksource.com_applications.yaml | 26 +++++ ...ion-control.flanksource.com_playbooks.yaml | 29 +++-- ...mission-control.flanksource.com_views.yaml | 54 ++++++++++ config/schemas/application.schema.json | 48 +++++++++ config/schemas/playbook-spec.schema.json | 25 ++--- config/schemas/playbook.schema.json | 25 ++--- config/schemas/view.schema.json | 101 +++++++++++++++++- fixtures/views/ui-ref-examples.yaml | 13 +++ rbac_report/report.go | 3 +- 16 files changed, 435 insertions(+), 81 deletions(-) diff --git a/api/application.go b/api/application.go index 460e3a4bc..2d9b309cc 100644 --- a/api/application.go +++ b/api/application.go @@ -8,21 +8,24 @@ import ( ) const ( - SectionTypeView = "view" - SectionTypeChanges = "changes" - SectionTypeConfigs = "configs" + SectionTypeView = "view" + SectionTypeChanges = "changes" + SectionTypeConfigs = "configs" + SectionTypeAccess = "access" + SectionTypeAccessLogs = "accessLogs" ) // ApplicationSection is a typed section in an application response. -// The Type field is one of "view", "changes", or "configs". // Only the field matching the type is populated. type ApplicationSection struct { - Type string `json:"type"` - Title string `json:"title"` - Icon string `json:"icon,omitempty"` - View *ApplicationViewData `json:"view,omitempty"` - Changes []ApplicationChange `json:"changes,omitempty"` - Configs []ApplicationConfigItem `json:"configs,omitempty"` + Type string `json:"type"` + Title string `json:"title"` + Icon string `json:"icon,omitempty"` + View *ApplicationViewData `json:"view,omitempty"` + Changes []ApplicationChange `json:"changes,omitempty"` + Configs []ApplicationConfigItem `json:"configs,omitempty"` + Access []AccessItem `json:"access,omitempty"` + AccessLogs []AccessLogItem `json:"accessLogs,omitempty"` } // ApplicationViewData holds the data-only fields from a resolved ViewRef section. @@ -47,6 +50,33 @@ type ApplicationConfigItem struct { Labels map[string]string `json:"labels,omitempty"` } +type AccessItem struct { + ConfigID string `json:"configId"` + ConfigName string `json:"configName"` + ConfigType string `json:"configType"` + UserID string `json:"userId"` + UserName string `json:"userName"` + Email string `json:"email"` + Role string `json:"role"` + UserType string `json:"userType"` + CreatedAt time.Time `json:"createdAt"` + LastSignedInAt *time.Time `json:"lastSignedInAt,omitempty"` + LastReviewedAt *time.Time `json:"lastReviewedAt,omitempty"` + IsStale bool `json:"isStale"` +} + +type AccessLogItem struct { + ConfigID string `json:"configId"` + ConfigName string `json:"configName"` + ConfigType string `json:"configType"` + UserID string `json:"userId"` + UserName string `json:"userName"` + CreatedAt time.Time `json:"createdAt"` + MFA bool `json:"mfa"` + Count int `json:"count"` + Properties map[string]string `json:"properties,omitempty"` +} + // Application is the schema that UI uses. type Application struct { ApplicationDetail `json:",inline"` diff --git a/api/v1/playbook_actions.go b/api/v1/playbook_actions.go index 15f2eea19..3de861949 100644 --- a/api/v1/playbook_actions.go +++ b/api/v1/playbook_actions.go @@ -670,12 +670,16 @@ type FacetPDFOptions struct { } type FacetOptions struct { - Connection string `json:"connection,omitempty" yaml:"connection,omitempty" template:"true"` - URL string `json:"url,omitempty" yaml:"url,omitempty" template:"true"` - PDFOptions *FacetPDFOptions `json:"pdfOptions,omitempty" yaml:"pdfOptions,omitempty"` - Header string `json:"header,omitempty" yaml:"header,omitempty" template:"true"` - Footer string `json:"footer,omitempty" yaml:"footer,omitempty" template:"true"` - TimestampURL string `json:"timestampUrl,omitempty" yaml:"timestampUrl,omitempty" template:"true"` + Connection string `json:"connection,omitempty" yaml:"connection,omitempty" template:"true"` + URL string `json:"url,omitempty" yaml:"url,omitempty" template:"true"` + FacetRenderOptions `json:",inline" yaml:",inline"` +} + +type FacetRenderOptions struct { + *FacetPDFOptions `json:",inline" yaml:",inline"` + Header string `json:"header,omitempty" yaml:"header,omitempty" template:"true"` + Footer string `json:"footer,omitempty" yaml:"footer,omitempty" template:"true"` + TimestampURL string `json:"timestampUrl,omitempty" yaml:"timestampUrl,omitempty" template:"true"` } // CatalogAction creates a config item in the catalog. diff --git a/api/v1/view_types.go b/api/v1/view_types.go index 45f2b5018..e382c2c6c 100644 --- a/api/v1/view_types.go +++ b/api/v1/view_types.go @@ -111,9 +111,7 @@ type ViewSpec struct { // Include other views in the view Sections []api.ViewSection `json:"sections,omitempty" yaml:"sections,omitempty"` - // MCP defines metadata for MCP tool registration, controlling how - // this view appears to LLM clients (Claude, Gemini, Codex). - MCP MCPMetadata `json:"mcp,omitempty" yaml:"mcp,omitempty"` + PDF *FacetOptions `json:"pdf,omitempty" yaml:"pdf,omitempty"` } type ViewQueryWithColumnDefs struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 7dd081748..6aedc972d 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -1596,11 +1596,7 @@ func (in *ExecAction) DeepCopy() *ExecAction { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FacetOptions) DeepCopyInto(out *FacetOptions) { *out = *in - if in.PDFOptions != nil { - in, out := &in.PDFOptions, &out.PDFOptions - *out = new(FacetPDFOptions) - (*in).DeepCopyInto(*out) - } + in.FacetRenderOptions.DeepCopyInto(&out.FacetRenderOptions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FacetOptions. @@ -1668,6 +1664,26 @@ func (in *FacetPDFOptions) DeepCopy() *FacetPDFOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FacetRenderOptions) DeepCopyInto(out *FacetRenderOptions) { + *out = *in + if in.FacetPDFOptions != nil { + in, out := &in.FacetPDFOptions, &out.FacetPDFOptions + *out = new(FacetPDFOptions) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FacetRenderOptions. +func (in *FacetRenderOptions) DeepCopy() *FacetRenderOptions { + if in == nil { + return nil + } + out := new(FacetRenderOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPConnection) DeepCopyInto(out *GCPConnection) { *out = *in @@ -3926,7 +3942,11 @@ func (in *ViewSpec) DeepCopyInto(out *ViewSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - in.MCP.DeepCopyInto(&out.MCP) + if in.PDF != nil { + in, out := &in.PDF, &out.PDF + *out = new(FacetOptions) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ViewSpec. diff --git a/api/view.go b/api/view.go index 80a4c44bc..6e2c60039 100644 --- a/api/view.go +++ b/api/view.go @@ -66,8 +66,10 @@ type ViewSection struct { // +kubebuilder:object:generate=true // UIRef references a native Flanksource UI component (changes or configs) type UIRef struct { - Changes *ChangesUIFilters `json:"changes,omitempty"` - Configs *ConfigsUIFilters `json:"configs,omitempty"` + Changes *ChangesUIFilters `json:"changes,omitempty"` + Configs *ConfigsUIFilters `json:"configs,omitempty"` + Access *AccessUIFilters `json:"access,omitempty"` + AccessLogs *AccessLogsUIFilters `json:"accessLogs,omitempty"` } // +kubebuilder:object:generate=true @@ -98,6 +100,24 @@ type ConfigsUIFilters struct { Health string `json:"health,omitempty"` // e.g. "-healthy,warning" } +// +kubebuilder:object:generate=true +type AccessUIFilters struct { + Search string `json:"search,omitempty"` // maps to ResourceSelector.Search for scoping configs + ConfigTypes string `json:"configTypes,omitempty"` // e.g. "AWS::IAM::Role,-Kubernetes::Pod" + Role string `json:"role,omitempty"` // e.g. "Owner,-Reader" + UserType string `json:"userType,omitempty"` // e.g. "Member,-Guest" + Stale string `json:"stale,omitempty"` // duration threshold, e.g. "2160h" (90 days) +} + +// +kubebuilder:object:generate=true +type AccessLogsUIFilters struct { + Search string `json:"search,omitempty"` // maps to ResourceSelector.Search for scoping configs + ConfigTypes string `json:"configTypes,omitempty"` // e.g. "AWS::IAM::Role" + From string `json:"from,omitempty"` // e.g. "720h" (30 days) + To string `json:"to,omitempty"` + MFA string `json:"mfa,omitempty"` // "true" or "false" +} + type SerializedView struct { SerializedSection `json:",inline"` Namespace string `json:"namespace,omitempty"` diff --git a/api/zz_generated.deepcopy.go b/api/zz_generated.deepcopy.go index e623fb80d..7727e26b1 100644 --- a/api/zz_generated.deepcopy.go +++ b/api/zz_generated.deepcopy.go @@ -9,6 +9,36 @@ import ( timex "time" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessLogsUIFilters) DeepCopyInto(out *AccessLogsUIFilters) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessLogsUIFilters. +func (in *AccessLogsUIFilters) DeepCopy() *AccessLogsUIFilters { + if in == nil { + return nil + } + out := new(AccessLogsUIFilters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessUIFilters) DeepCopyInto(out *AccessUIFilters) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessUIFilters. +func (in *AccessUIFilters) DeepCopy() *AccessUIFilters { + if in == nil { + return nil + } + out := new(AccessUIFilters) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutoClose) DeepCopyInto(out *AutoClose) { *out = *in @@ -630,6 +660,16 @@ func (in *UIRef) DeepCopyInto(out *UIRef) { *out = new(ConfigsUIFilters) **out = **in } + if in.Access != nil { + in, out := &in.Access, &out.Access + *out = new(AccessUIFilters) + **out = **in + } + if in.AccessLogs != nil { + in, out := &in.AccessLogs, &out.AccessLogs + *out = new(AccessLogsUIFilters) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UIRef. diff --git a/application/application.go b/application/application.go index a55a31230..ae5a56368 100644 --- a/application/application.go +++ b/application/application.go @@ -184,6 +184,32 @@ func buildSection(ctx context.Context, section api.ViewSection) (api.Application return appSection, nil } + if section.UIRef.Access != nil { + appSection.Type = api.SectionTypeAccess + access, err := db.GetAccessForUIRef(ctx, section.UIRef.Access) + if err != nil { + return appSection, ctx.Oops().Errorf("failed to get access for section %q: %w", section.Title, err) + } + if access == nil { + access = []api.AccessItem{} + } + appSection.Access = access + return appSection, nil + } + + if section.UIRef.AccessLogs != nil { + appSection.Type = api.SectionTypeAccessLogs + logs, err := db.GetAccessLogsForUIRef(ctx, section.UIRef.AccessLogs) + if err != nil { + return appSection, ctx.Oops().Errorf("failed to get access logs for section %q: %w", section.Title, err) + } + if logs == nil { + logs = []api.AccessLogItem{} + } + appSection.AccessLogs = logs + return appSection, nil + } + return appSection, nil } diff --git a/config/crds/mission-control.flanksource.com_applications.yaml b/config/crds/mission-control.flanksource.com_applications.yaml index 551c8ae8b..790273f09 100644 --- a/config/crds/mission-control.flanksource.com_applications.yaml +++ b/config/crds/mission-control.flanksource.com_applications.yaml @@ -448,6 +448,32 @@ spec: description: UIRef references a native Flanksource UI component (changes or configs) properties: + access: + properties: + configTypes: + type: string + role: + type: string + search: + type: string + stale: + type: string + userType: + type: string + type: object + accessLogs: + properties: + configTypes: + type: string + from: + type: string + mfa: + type: string + search: + type: string + to: + type: string + type: object changes: description: |- ChangesUIFilters defines filters for the native Changes UI component. diff --git a/config/crds/mission-control.flanksource.com_playbooks.yaml b/config/crds/mission-control.flanksource.com_playbooks.yaml index 2ba338356..a3beb0a80 100644 --- a/config/crds/mission-control.flanksource.com_playbooks.yaml +++ b/config/crds/mission-control.flanksource.com_playbooks.yaml @@ -4255,24 +4255,21 @@ spec: type: string header: type: string - pdfOptions: + landscape: + type: boolean + margins: properties: - landscape: - type: boolean - margins: - properties: - bottom: - type: integer - left: - type: integer - right: - type: integer - top: - type: integer - type: object - pageSize: - type: string + bottom: + type: integer + left: + type: integer + right: + type: integer + top: + type: integer type: object + pageSize: + type: string timestampUrl: type: string url: diff --git a/config/crds/mission-control.flanksource.com_views.yaml b/config/crds/mission-control.flanksource.com_views.yaml index 6a9d73c4d..c71a8151c 100644 --- a/config/crds/mission-control.flanksource.com_views.yaml +++ b/config/crds/mission-control.flanksource.com_views.yaml @@ -613,6 +613,34 @@ spec: - message: heatmap config not allowed for this type rule: 'self.type!=''heatmap'' ? !has(self.heatmap) : true' type: array + pdf: + properties: + connection: + type: string + footer: + type: string + header: + type: string + landscape: + type: boolean + margins: + properties: + bottom: + type: integer + left: + type: integer + right: + type: integer + top: + type: integer + type: object + pageSize: + type: string + timestampUrl: + type: string + url: + type: string + type: object queries: additionalProperties: properties: @@ -2153,6 +2181,32 @@ spec: description: UIRef references a native Flanksource UI component (changes or configs) properties: + access: + properties: + configTypes: + type: string + role: + type: string + search: + type: string + stale: + type: string + userType: + type: string + type: object + accessLogs: + properties: + configTypes: + type: string + from: + type: string + mfa: + type: string + search: + type: string + to: + type: string + type: object changes: description: |- ChangesUIFilters defines filters for the native Changes UI component. diff --git a/config/schemas/application.schema.json b/config/schemas/application.schema.json index 5a2f175df..1ac09bd18 100644 --- a/config/schemas/application.schema.json +++ b/config/schemas/application.schema.json @@ -3,6 +3,48 @@ "$id": "https://github.com/flanksource/incident-commander/api/v1/application", "$ref": "#/$defs/Application", "$defs": { + "AccessLogsUIFilters": { + "properties": { + "search": { + "type": "string" + }, + "configTypes": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "mfa": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "AccessUIFilters": { + "properties": { + "search": { + "type": "string" + }, + "configTypes": { + "type": "string" + }, + "role": { + "type": "string" + }, + "userType": { + "type": "string" + }, + "stale": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "Application": { "properties": { "kind": { @@ -582,6 +624,12 @@ }, "configs": { "$ref": "#/$defs/ConfigsUIFilters" + }, + "access": { + "$ref": "#/$defs/AccessUIFilters" + }, + "accessLogs": { + "$ref": "#/$defs/AccessLogsUIFilters" } }, "additionalProperties": false, diff --git a/config/schemas/playbook-spec.schema.json b/config/schemas/playbook-spec.schema.json index 912a184e3..71e59151c 100644 --- a/config/schemas/playbook-spec.schema.json +++ b/config/schemas/playbook-spec.schema.json @@ -582,8 +582,14 @@ "url": { "type": "string" }, - "pdfOptions": { - "$ref": "#/$defs/FacetPDFOptions" + "pageSize": { + "type": "string" + }, + "landscape": { + "type": "boolean" + }, + "margins": { + "$ref": "#/$defs/FacetPDFMargins" }, "header": { "type": "string" @@ -616,21 +622,6 @@ "additionalProperties": false, "type": "object" }, - "FacetPDFOptions": { - "properties": { - "pageSize": { - "type": "string" - }, - "landscape": { - "type": "boolean" - }, - "margins": { - "$ref": "#/$defs/FacetPDFMargins" - } - }, - "additionalProperties": false, - "type": "object" - }, "FieldMappingConfig": { "properties": { "id": { diff --git a/config/schemas/playbook.schema.json b/config/schemas/playbook.schema.json index 107dce2e3..132c445a1 100644 --- a/config/schemas/playbook.schema.json +++ b/config/schemas/playbook.schema.json @@ -613,8 +613,14 @@ "url": { "type": "string" }, - "pdfOptions": { - "$ref": "#/$defs/FacetPDFOptions" + "pageSize": { + "type": "string" + }, + "landscape": { + "type": "boolean" + }, + "margins": { + "$ref": "#/$defs/FacetPDFMargins" }, "header": { "type": "string" @@ -647,21 +653,6 @@ "additionalProperties": false, "type": "object" }, - "FacetPDFOptions": { - "properties": { - "pageSize": { - "type": "string" - }, - "landscape": { - "type": "boolean" - }, - "margins": { - "$ref": "#/$defs/FacetPDFMargins" - } - }, - "additionalProperties": false, - "type": "object" - }, "FieldMappingConfig": { "properties": { "id": { diff --git a/config/schemas/view.schema.json b/config/schemas/view.schema.json index 913860ee8..d6ed86a7a 100644 --- a/config/schemas/view.schema.json +++ b/config/schemas/view.schema.json @@ -36,6 +36,48 @@ "additionalProperties": false, "type": "object" }, + "AccessLogsUIFilters": { + "properties": { + "search": { + "type": "string" + }, + "configTypes": { + "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "mfa": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "AccessUIFilters": { + "properties": { + "search": { + "type": "string" + }, + "configTypes": { + "type": "string" + }, + "role": { + "type": "string" + }, + "userType": { + "type": "string" + }, + "stale": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "BadgeConfig": { "properties": { "color": { @@ -323,6 +365,54 @@ "additionalProperties": false, "type": "object" }, + "FacetOptions": { + "properties": { + "connection": { + "type": "string" + }, + "url": { + "type": "string" + }, + "pageSize": { + "type": "string" + }, + "landscape": { + "type": "boolean" + }, + "margins": { + "$ref": "#/$defs/FacetPDFMargins" + }, + "header": { + "type": "string" + }, + "footer": { + "type": "string" + }, + "timestampUrl": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "FacetPDFMargins": { + "properties": { + "top": { + "type": "integer" + }, + "bottom": { + "type": "integer" + }, + "left": { + "type": "integer" + }, + "right": { + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + }, "FieldsV1": { "properties": {}, "additionalProperties": false, @@ -1012,6 +1102,12 @@ }, "configs": { "$ref": "#/$defs/ConfigsUIFilters" + }, + "access": { + "$ref": "#/$defs/AccessUIFilters" + }, + "accessLogs": { + "$ref": "#/$defs/AccessLogsUIFilters" } }, "additionalProperties": false, @@ -1266,9 +1362,8 @@ "type": "array", "description": "Include other views in the view" }, - "mcp": { - "$ref": "#/$defs/MCPMetadata", - "description": "MCP defines metadata for MCP tool registration, controlling how\nthis view appears to LLM clients (Claude, Gemini, Codex)." + "pdf": { + "$ref": "#/$defs/FacetOptions" } }, "additionalProperties": false, diff --git a/fixtures/views/ui-ref-examples.yaml b/fixtures/views/ui-ref-examples.yaml index fee828683..137ba7c32 100644 --- a/fixtures/views/ui-ref-examples.yaml +++ b/fixtures/views/ui-ref-examples.yaml @@ -22,3 +22,16 @@ spec: uiRef: configs: health: -healthy + + - title: Stale Access + icon: shield + uiRef: + access: + stale: "2160h" + + - title: Recent Sign-ins (No MFA) + icon: key + uiRef: + accessLogs: + from: "720h" + mfa: "false" diff --git a/rbac_report/report.go b/rbac_report/report.go index c5803ea0d..70f4a1737 100644 --- a/rbac_report/report.go +++ b/rbac_report/report.go @@ -19,6 +19,7 @@ import ( type Options struct { Title string Selectors []types.ResourceSelector + Recursive bool StaleDays int ReviewOverdueDays int ChangelogSince time.Duration @@ -44,7 +45,7 @@ func (o Options) WithDefaults() Options { func BuildReport(ctx context.Context, opts Options) (*api.RBACReport, error) { opts = opts.WithDefaults() - rows, err := db.GetRBACAccess(ctx, opts.Selectors) + rows, err := db.GetRBACAccess(ctx, opts.Selectors, opts.Recursive) if err != nil { return nil, ctx.Oops().Wrapf(err, "failed to query RBAC access") } From 7d0d62335d94882ad4e538acd6a03535b557e764 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 5 Apr 2026 17:37:11 +0300 Subject: [PATCH 24/48] feat(report): add catalog and RBAC matrix report components Introduces CatalogReport, CatalogReportPage, KitchenSink, MatrixDemo, and RBACMatrixReport components for rendering comprehensive PDF reports. Includes new shared components for config changes, insights, relationships, access control, and RBAC matrix visualization with identity types, access patterns, and staleness indicators. Renames internal CoverPage functions to CoverContent to avoid confusion with Page component. --- report/.gitignore | 1 + report/Application.tsx | 4 +- report/CatalogReport.tsx | 198 +++++ report/KitchenSink.tsx | 279 ++++++ report/MatrixDemo.tsx | 216 +++++ report/RBACByUserReport.tsx | 39 +- report/RBACMatrixReport.tsx | 108 +++ report/RBACReport.tsx | 97 -- report/ViewReport.tsx | 4 +- report/catalog-report-types.ts | 61 ++ .../components/CatalogAccessLogsSection.tsx | 53 ++ report/components/CatalogAccessSection.tsx | 61 ++ report/components/ConfigChangesSection.tsx | 78 ++ report/components/ConfigInsightsSection.tsx | 116 +++ report/components/ConfigItemCard.tsx | 32 + report/components/ConfigLink.tsx | 24 + report/components/ConfigRelationshipGraph.tsx | 84 ++ report/components/RBACCoverContent.tsx | 93 ++ report/components/RBACMatrixSection.tsx | 153 ++++ report/components/RBACResourceSection.tsx | 292 ------ report/components/ScraperCard.tsx | 89 ++ report/components/rbac-visual.tsx | 120 +++ report/config-types.ts | 63 ++ report/kitchen-sink-data.ts | 11 + report/package-lock.json | 832 ++---------------- report/package.json | 2 +- report/rbac-types.ts | 23 + report/scraper-types.ts | 27 + report/testdata/kitchen-sink.yaml | 393 +++++++++ 29 files changed, 2359 insertions(+), 1194 deletions(-) create mode 100644 report/CatalogReport.tsx create mode 100644 report/KitchenSink.tsx create mode 100644 report/MatrixDemo.tsx create mode 100644 report/RBACMatrixReport.tsx delete mode 100644 report/RBACReport.tsx create mode 100644 report/catalog-report-types.ts create mode 100644 report/components/CatalogAccessLogsSection.tsx create mode 100644 report/components/CatalogAccessSection.tsx create mode 100644 report/components/ConfigChangesSection.tsx create mode 100644 report/components/ConfigInsightsSection.tsx create mode 100644 report/components/ConfigItemCard.tsx create mode 100644 report/components/ConfigLink.tsx create mode 100644 report/components/ConfigRelationshipGraph.tsx create mode 100644 report/components/RBACCoverContent.tsx create mode 100644 report/components/RBACMatrixSection.tsx delete mode 100644 report/components/RBACResourceSection.tsx create mode 100644 report/components/ScraperCard.tsx create mode 100644 report/components/rbac-visual.tsx create mode 100644 report/config-types.ts create mode 100644 report/kitchen-sink-data.ts create mode 100644 report/scraper-types.ts create mode 100644 report/testdata/kitchen-sink.yaml diff --git a/report/.gitignore b/report/.gitignore index 4c6b715d8..493ec8ea7 100644 --- a/report/.gitignore +++ b/report/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ .facet/ out.* +.playwright-mcp/ diff --git a/report/Application.tsx b/report/Application.tsx index a1741f368..8dee6c7ab 100644 --- a/report/Application.tsx +++ b/report/Application.tsx @@ -29,7 +29,7 @@ function PageFooter() { ); } -function CoverPage({ app }: { app: Application }) { +function CoverContent({ app }: { app: Application }) { const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); @@ -82,7 +82,7 @@ export default function ApplicationReport({ data }: ApplicationReportProps) { <> {/* Cover page — no header/footer */} - + diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx new file mode 100644 index 000000000..f868550dd --- /dev/null +++ b/report/CatalogReport.tsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { Page, PageBreak, Section } from '@flanksource/facet'; +import { Icon } from '@flanksource/icons/icon'; +import type { CatalogReportData } from './catalog-report-types.ts'; +import ConfigChangesSection from './components/ConfigChangesSection.tsx'; +import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; +import ConfigRelationshipGraph from './components/ConfigRelationshipGraph.tsx'; +import CatalogAccessSection from './components/CatalogAccessSection.tsx'; +import CatalogAccessLogsSection from './components/CatalogAccessLogsSection.tsx'; +import { formatDate, formatDateTime } from './components/utils.ts'; + +function PageHeader({ title }: { title: string }) { + return ( +
+ {title} + Catalog Report +
+ ); +} + +function PageFooter() { + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); + return ( +
+ Generated {now} +
+ ); +} + +function CoverPage({ data }: { data: CatalogReportData }) { + const ci = data.configItem; + const tags = { ...ci.tags, ...ci.labels }; + + return ( +
+
{data.title || 'Catalog Report'}
+ + {data.parents.length > 0 && ( +
+ {data.parents.map((p, i) => ( + + {i > 0 && ' › '} + {p.name} + + ))} +
+ )} + +
+ {ci.type && } + {ci.name} +
+ + {ci.type &&
{ci.type}
} + + {Object.keys(tags).length > 0 && ( +
+ {Object.entries(tags).map(([k, v]) => ( + + {k} + {v || '-'} + + ))} +
+ )} + +
+ {ci.health && Health: {ci.health}} + {ci.status && Status: {ci.status}} + {ci.created_at && Created: {formatDate(ci.created_at)}} +
+ +
+ Generated {data.generatedAt ? formatDateTime(data.generatedAt) : new Date().toLocaleDateString()} +
+ +
+ {data.sections.changes && {data.changes.length} changes} + {data.sections.insights && {data.analyses.length} insights} + {data.sections.relationships && {data.relatedConfigs.length} relationships} + {data.sections.access && {data.access.length} access entries} + {data.sections.accessLogs && {data.accessLogs.length} access logs} +
+
+ ); +} + +function ConfigJSONSection({ json }: { json: string }) { + let formatted = json; + try { + formatted = JSON.stringify(JSON.parse(json), null, 2); + } catch {} + + return ( +
+
+        {formatted}
+      
+
+ ); +} + +interface CatalogReportProps { + data: CatalogReportData; +} + +export default function CatalogReportPage({ data }: CatalogReportProps) { + const header = ; + const footer = ; + const pageProps = { + pageSize: 'a4' as const, + margins: { top: 1, bottom: 1, left: 5, right: 5 }, + header, + headerHeight: 8, + footer, + footerHeight: 8, + }; + + const configItem = { + id: data.configItem.id, + name: data.configItem.name, + type: data.configItem.type, + configClass: data.configItem.configClass, + status: data.configItem.status, + health: data.configItem.health, + description: data.configItem.description, + labels: data.configItem.labels, + tags: data.configItem.tags, + createdAt: data.configItem.created_at, + updatedAt: data.configItem.updated_at, + }; + + return ( + <> + + + + + {data.sections.changes && data.changes.length > 0 && ( + <> + + + + + + )} + + {data.sections.insights && data.analyses.length > 0 && ( + <> + + + + + + )} + + {data.sections.relationships && data.relatedConfigs.length > 0 && ( + <> + + + + + + )} + + {data.sections.access && data.access.length > 0 && ( + <> + + + + + + )} + + {data.sections.accessLogs && data.accessLogs.length > 0 && ( + <> + + + + + + )} + + {data.sections.configJSON && data.configJSON && ( + <> + + + + + + )} + + ); +} diff --git a/report/KitchenSink.tsx b/report/KitchenSink.tsx new file mode 100644 index 000000000..858d83591 --- /dev/null +++ b/report/KitchenSink.tsx @@ -0,0 +1,279 @@ +import React from 'react'; +import { Page, PageBreak, Section } from '@flanksource/facet'; +import type { ConfigReportData } from './config-types.ts'; +import ConfigLink from './components/ConfigLink.tsx'; +import ConfigChangesSection from './components/ConfigChangesSection.tsx'; +import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; +import ConfigRelationshipGraph from './components/ConfigRelationshipGraph.tsx'; +import ConfigItemCard from './components/ConfigItemCard.tsx'; +import ScraperCard from './components/ScraperCard.tsx'; +import { MatrixTable, Dot } from '@flanksource/facet'; +import type { ScraperInfo } from './scraper-types.ts'; + +const defaultData: ConfigReportData = { + configItem: { + id: 'cfg-eks-001', name: 'prod-eks-cluster', type: 'AWS::EKS::Cluster', + configClass: 'Cluster', status: 'Active', health: 'healthy', + description: 'Production EKS cluster running Mission Control workloads in us-east-1', + labels: { env: 'production', team: 'platform', region: 'us-east-1' }, + costTotal30d: 4280.50, createdAt: '2025-03-15T09:00:00Z', updatedAt: '2026-03-28T12:00:00Z', + }, + changes: [ + { id: 'chg-001', configID: 'cfg-eks-001', changeType: 'diff', severity: 'info', source: 'kubernetes', summary: 'Node pool autoscaler adjusted desired count from 3 to 5', createdBy: 'cluster-autoscaler', createdAt: '2026-03-30T08:15:00Z', count: 1 }, + { id: 'chg-002', configID: 'cfg-eks-001', changeType: 'Pulled', severity: 'info', source: 'kubernetes', summary: 'Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42', createdAt: '2026-03-30T07:30:00Z', count: 3 }, + { id: 'chg-003', configID: 'cfg-eks-001', changeType: 'ScalingReplicaSet', severity: 'low', source: 'kubernetes', summary: 'Deployment incident-commander scaled from 2 to 3 replicas', externalCreatedBy: 'hpa-controller', createdAt: '2026-03-29T22:00:00Z' }, + { id: 'chg-004', configID: 'cfg-eks-001', changeType: 'diff', severity: 'medium', source: 'terraform', summary: 'EKS cluster version upgraded from 1.28 to 1.29', createdBy: 'alice@flanksource.com', createdAt: '2026-03-29T14:00:00Z' }, + { id: 'chg-005', configID: 'cfg-eks-001', changeType: 'PolicyUpdate', severity: 'high', source: 'argocd', summary: 'Network policy updated: restricted egress to 10.0.0.0/8 for namespace mc', createdBy: 'bob@flanksource.com', createdAt: '2026-03-28T16:00:00Z' }, + { id: 'chg-006', configID: 'cfg-eks-001', changeType: 'diff', severity: 'critical', source: 'aws-config', summary: 'IAM role policy detached: eks-admin-access removed from cluster role', createdBy: 'security-automation', createdAt: '2026-03-28T10:00:00Z' }, + { id: 'chg-007', configID: 'cfg-eks-001', changeType: 'FieldsV1', severity: 'info', source: 'kubernetes', summary: 'ConfigMap kube-proxy updated with new CIDR ranges', createdAt: '2026-03-27T18:00:00Z', count: 2 }, + { id: 'chg-008', configID: 'cfg-eks-001', changeType: 'diff', severity: 'low', source: 'terraform', summary: 'Added tag cost-center=platform-engineering to cluster', createdBy: 'carol@flanksource.com', createdAt: '2026-03-27T09:00:00Z' }, + ], + analyses: [ + { id: 'ana-001', configID: 'cfg-eks-001', analyzer: 'Trivy', message: 'Container image flanksource/incident-commander:v1.4.200 has 3 high CVEs', status: 'open', severity: 'high', analysisType: 'security', source: 'trivy-operator', firstObserved: '2026-03-28T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + { id: 'ana-002', configID: 'cfg-eks-001', analyzer: 'Trivy', message: 'Base image golang:1.23-alpine has known vulnerability in libcrypto (CVE-2026-0891)', status: 'open', severity: 'critical', analysisType: 'security', source: 'trivy-operator', firstObserved: '2026-03-25T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + { id: 'ana-003', configID: 'cfg-eks-001', analyzer: 'OPA/Gatekeeper', message: 'Pod incident-commander-7f8b9c running as root user in namespace mc', status: 'open', severity: 'medium', analysisType: 'compliance', source: 'gatekeeper', firstObserved: '2026-03-20T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + { id: 'ana-004', configID: 'cfg-eks-001', analyzer: 'OPA/Gatekeeper', message: 'Namespace mc missing required label: data-classification', status: 'silenced', severity: 'low', analysisType: 'compliance', source: 'gatekeeper', firstObserved: '2026-03-15T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + { id: 'ana-005', configID: 'cfg-eks-001', analyzer: 'AWS Cost Optimizer', message: 'EKS node group i3.xlarge instances are underutilized (avg CPU 18%)', status: 'open', severity: 'medium', analysisType: 'cost', source: 'aws-cost-explorer', firstObserved: '2026-03-01T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + { id: 'ana-007', configID: 'cfg-eks-001', analyzer: 'Prometheus Advisor', message: 'P99 API response latency exceeded 500ms threshold 12 times in the last 7 days', status: 'open', severity: 'high', analysisType: 'performance', source: 'prometheus', firstObserved: '2026-03-23T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + { id: 'ana-010', configID: 'cfg-eks-001', analyzer: 'Prometheus Advisor', message: 'Node ip-10-0-2-18 memory utilization consistently above 85%', status: 'open', severity: 'high', analysisType: 'reliability', source: 'prometheus', firstObserved: '2026-03-26T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, + ], + relationships: [ + { configID: 'cfg-eks-001', relatedID: 'cfg-vpc-001', relation: 'RunsIn', direction: 'outgoing' }, + { configID: 'cfg-eks-001', relatedID: 'cfg-iam-001', relation: 'ManagedBy', direction: 'outgoing' }, + { configID: 'cfg-eks-001', relatedID: 'cfg-sg-001', relation: 'DependsOn', direction: 'outgoing' }, + { configID: 'cfg-eks-001', relatedID: 'cfg-rds-001', relation: 'DependsOn', direction: 'outgoing' }, + { configID: 'cfg-deploy-001', relatedID: 'cfg-eks-001', relation: 'RunsOn', direction: 'incoming' }, + { configID: 'cfg-deploy-002', relatedID: 'cfg-eks-001', relation: 'RunsOn', direction: 'incoming' }, + { configID: 'cfg-deploy-003', relatedID: 'cfg-eks-001', relation: 'RunsOn', direction: 'incoming' }, + { configID: 'cfg-ns-001', relatedID: 'cfg-eks-001', relation: 'ChildOf', direction: 'incoming' }, + { configID: 'cfg-node-001', relatedID: 'cfg-eks-001', relation: 'ChildOf', direction: 'incoming' }, + { configID: 'cfg-node-002', relatedID: 'cfg-eks-001', relation: 'ChildOf', direction: 'incoming' }, + ], + relatedConfigs: [ + { id: 'cfg-vpc-001', name: 'prod-vpc', type: 'AWS::EC2::VPC', configClass: 'Network', status: 'available', health: 'healthy', labels: { env: 'production' } }, + { id: 'cfg-iam-001', name: 'eks-cluster-role', type: 'AWS::IAM::Role', configClass: 'IAM', status: 'active', health: 'healthy' }, + { id: 'cfg-sg-001', name: 'eks-cluster-sg', type: 'AWS::EC2::SecurityGroup', configClass: 'Network', status: 'active', health: 'warning', labels: { env: 'production' } }, + { id: 'cfg-rds-001', name: 'mission-control-db', type: 'AWS::RDS::Instance', configClass: 'Database', status: 'available', health: 'healthy', labels: { env: 'production', engine: 'postgresql' } }, + { id: 'cfg-deploy-001', name: 'incident-commander', type: 'Kubernetes::Deployment', configClass: 'Deployment', status: 'Running', health: 'healthy', labels: { app: 'incident-commander' } }, + { id: 'cfg-deploy-002', name: 'canary-checker', type: 'Kubernetes::Deployment', configClass: 'Deployment', status: 'Running', health: 'healthy', labels: { app: 'canary-checker' } }, + { id: 'cfg-deploy-003', name: 'config-db', type: 'Kubernetes::Deployment', configClass: 'Deployment', status: 'Running', health: 'unhealthy', labels: { app: 'config-db' } }, + { id: 'cfg-ns-001', name: 'mc', type: 'Kubernetes::Namespace', configClass: 'Namespace', status: 'Active', health: 'healthy' }, + { id: 'cfg-node-001', name: 'ip-10-0-1-42', type: 'Kubernetes::Node', configClass: 'Node', status: 'Ready', health: 'healthy' }, + { id: 'cfg-node-002', name: 'ip-10-0-2-18', type: 'Kubernetes::Node', configClass: 'Node', status: 'Ready', health: 'warning', labels: { 'instance-type': 'i3.xlarge' } }, + ], +}; + +function PageHeader() { + return ( +
+ Config Components + Kitchen Sink +
+ ); +} + +function PageFooter() { + const date = new Date().toLocaleDateString('en-US', { + year: 'numeric', month: 'long', day: 'numeric', + }); + return ( +
+ Generated {date} +
+ ); +} + +function CoverContent() { + const date = new Date().toLocaleDateString('en-US', { + year: 'numeric', month: 'long', day: 'numeric', + }); + return ( +
+
+
+ Component Showcase +
+

+ Config Components +

+
+ Kitchen Sink +
+

+ PDF-compatible components for rendering config items, changes, insights, and relationships. +

+
+
+
Generated on {date}
+
+ ); +} + +interface KitchenSinkProps { + data?: ConfigReportData; +} + +export default function KitchenSink({ data: externalData }: KitchenSinkProps) { + const data = externalData?.configItem ? externalData : defaultData; + const header = ; + const footer = ; + const pageProps = { + pageSize: 'a4' as const, + margins: { top: 5, bottom: 5, left: 5, right: 5 }, + header, + headerHeight: 10, + footer, + footerHeight: 10, + }; + + const sampleConfigs = [data.configItem, ...data.relatedConfigs.slice(0, 5)]; + + const sampleScrapers: ScraperInfo[] = [ + { + id: 'scr-001', name: 'mc/aws-production', namespace: 'mc', + source: 'KubernetesCRD', types: ['aws', 'kubernetes'], + specHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd', + createdBy: 'alice@flanksource.com', createdAt: '2025-06-10T09:00:00Z', updatedAt: '2026-03-28T14:30:00Z', + gitops: { + git: { url: 'https://github.com/flanksource/mission-control-demo', branch: 'main', file: 'clusters/prod/scrapers/aws.yaml', dir: 'clusters/prod/scrapers', link: 'https://github.com/flanksource/mission-control-demo/tree/main/clusters/prod/scrapers/aws.yaml' }, + kustomize: { path: 'clusters/prod/scrapers', file: 'clusters/prod/scrapers/kustomization.yaml' }, + }, + }, + { + id: 'scr-002', name: 'mc/azure-entra', namespace: 'mc', + source: 'KubernetesCRD', types: ['azure'], + specHash: 'ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33', + createdAt: '2025-09-01T10:00:00Z', updatedAt: '2026-03-30T08:00:00Z', + }, + { + id: 'scr-003', name: 'local-file-scraper', + source: 'ConfigFile', types: ['file', 'sql'], + specHash: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + createdBy: 'bob@flanksource.com', createdAt: '2026-01-15T11:00:00Z', + }, + ]; + + return ( + <> + + + + + + + +
+
+
+ Renders a config item as Icon + Name with optional health indicator. +
+
+ {sampleConfigs.map((config) => ( +
+
+ +
+
+ +
+ {config.type} +
+ ))} +
+
+
+
+ + + + +
+
+ Renders a config item with icon, name, tags, and metadata. +
+
+ {sampleConfigs.map((config) => ( +
+ +
+ ))} +
+
+
+ + + + +
+
+ Renders a scraper with type icons, source badge, spec hash, created by, dates, and GitOps provenance. +
+
+ {sampleScrapers.map((scraper) => ( + + ))} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ Rotated column headers using CSS-Tricks translate+rotate pattern. +
+ alice@example.com, cells: [, , , , null, null] }, + { label: bob@example.com, cells: [, , null, null, null, null] }, + { label: charlie@example.com, cells: [, null, null, null, null, ] }, + { label: deploy-bot, cells: [, , , null, null, null] }, + { label: monitoring-svc, cells: [, null, null, null, null, ] }, + ]} + /> +
+ With longer column names and more rows. +
+ design-studio-pas, cells: [null, null, , null, null, null, null] }, + { label: monitoring_ro, cells: [, null, null, null, null, null, null] }, + { label: oipa-qa-bot, cells: [null, null, , null, null, null, null] }, + { label: omasa, cells: [null, null, , null, null, null, null] }, + { label: SG-OMAR Shared Dev DB, cells: [null, , null, null, , null, null] }, + { label: SG-OMAR Shared RO, cells: [, null, null, null, null, null, ] }, + { label: svc_mission_control, cells: [, , null, null, null, null, null] }, + ]} + /> +
+
+ + ); +} diff --git a/report/MatrixDemo.tsx b/report/MatrixDemo.tsx new file mode 100644 index 000000000..7882711c4 --- /dev/null +++ b/report/MatrixDemo.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import { Page } from '@flanksource/facet'; +import { MatrixTable, Dot } from '@flanksource/facet'; +import { + AccessIndicator, + IdentityIcon, + identityType, + ACCESS_COLORS, + STALE_COLORS, + ReviewOverdueBadge, + ReviewOverdueLegendSwatch, +} from './components/rbac-visual.tsx'; + +function Cell({ direct, overdue }: { direct: boolean; overdue?: boolean }) { + const color = direct ? ACCESS_COLORS.direct : ACCESS_COLORS.group; + return ( +
+ + {overdue && } +
+ ); +} + +function UserLabel({ name, userId, roleSource, staleBorderColor }: { name: string; userId: string; roleSource?: string; staleBorderColor?: string }) { + return ( + + + {name} + + ); +} + +export default function MatrixDemo() { + const roles = ['Read', 'Write', 'Execute', 'Admin', 'Delete', 'Audit']; + + return ( + +

+ RBAC Matrix - Visual System Demo +

+ + {/* --- Reference Section --- */} +
+
+ Identity Types +
+
+ {(['user', 'group', 'service', 'bot'] as const).map((type) => ( + + + {identityType( + type === 'service' ? 'svc-x' : type === 'bot' ? 'bot-x' : 'x', + type === 'group' ? 'group:x' : 'direct', + ).label} + + ))} +
+
+ +
+
+ Access Pattern - Filled vs Unfilled +
+
+ + Direct (filled) + + + Indirect / Group (unfilled) + +
+
+ + {/* --- Matrix 1: Simple Permissions --- */} +
+ Simple Permissions +
+ , + cells: [ + , + , + , + , + null, + null, + ], + }, + { + label: , + cells: [ + , + , + null, null, null, null, + ], + }, + { + label: , + cells: [ + , + null, null, null, null, + , + ], + }, + { + label: , + cells: [ + , + , + , + null, null, null, + ], + }, + { + label: , + cells: [ + , + null, null, null, null, + , + ], + }, + { + label: , + cells: [ + null, null, null, + , + , + null, + ], + }, + { + label: , + cells: [ + , + , + null, null, null, null, + ], + }, + ]} + /> + + {/* --- Matrix 2: Database Roles --- */} +
+ Database Roles +
+ {(() => { + const dbRoles = ['db_datareader', 'db_datawriter', 'db_owner', 'db_securityadmin', 'db_backupoperator', 'db_ddladmin', 'db_accessadmin']; + return ( + , + cells: [null, null, , null, null, null, null], + }, + { + label: , + cells: [, null, null, null, null, null, null], + }, + { + label: , + cells: [null, null, , null, null, null, null], + }, + { + label: , + cells: [null, , null, null, , null, null], + }, + { + label: , + cells: [, null, null, null, null, null, ], + }, + { + label: , + cells: [, , null, null, null, null, null], + }, + ]} + /> + ); + })()} + + {/* --- Legend --- */} +
+ Access: + + Direct + + + Indirect + + + Last Login: + + + > 7d + + + + > 30d + + + Review: + +
+
+ ); +} diff --git a/report/RBACByUserReport.tsx b/report/RBACByUserReport.tsx index 5e32879dd..e1dc75645 100644 --- a/report/RBACByUserReport.tsx +++ b/report/RBACByUserReport.tsx @@ -4,6 +4,8 @@ import type { RBACReport } from './rbac-types.ts'; import RBACSummarySection from './components/RBACSummarySection.tsx'; import RBACUserSection from './components/RBACUserSection.tsx'; import RBACChangelogSection from './components/RBACChangelogSection.tsx'; +import RBACCoverContent from './components/RBACCoverContent.tsx'; +import { MatrixLegend } from './components/RBACMatrixSection.tsx'; function PageHeader({ title }: { title: string }) { return ( @@ -17,31 +19,11 @@ function PageHeader({ title }: { title: string }) { function PageFooter() { const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); return ( -
- Generated {now} -
- ); -} - -function CoverPage({ title, query }: { title: string; query?: string }) { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - return ( -
-
-
- RBAC Report - By User -
-

- {title} -

- {query && ( -
- {query} -
- )} +
+ +
+ Generated {now}
-
-
Generated {now}
); } @@ -55,20 +37,19 @@ export default function RBACByUserReportPage({ data }: Props) { const footer = ; const pageProps = { pageSize: 'a4-landscape' as const, - margins: { top: 1, bottom: 1, left: 0, right: 0 }, + margins: { top: 1, bottom: 1, left: 5, right: 5 }, header, headerHeight: 8, footer, - footerHeight: 8, + footerHeight: 14, }; const users = data.users || []; return ( <> - - - + + diff --git a/report/RBACMatrixReport.tsx b/report/RBACMatrixReport.tsx new file mode 100644 index 000000000..2b36a6609 --- /dev/null +++ b/report/RBACMatrixReport.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { Page, PageBreak } from '@flanksource/facet'; +import type { RBACReport, RBACResource } from './rbac-types.ts'; +import RBACSummarySection from './components/RBACSummarySection.tsx'; +import RBACMatrixSection, { MatrixLegend } from './components/RBACMatrixSection.tsx'; +import RBACChangelogSection from './components/RBACChangelogSection.tsx'; +import RBACCoverContent from './components/RBACCoverContent.tsx'; + +function PageHeader({ title }: { title: string }) { + return ( +
+ {title} + RBAC Matrix +
+ ); +} + +function PageFooter() { + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); + return ( +
+ +
+ Generated {now} +
+
+ ); +} + +function estimateHeight(resource: RBACResource): number { + const uniqueUsers = new Set(resource.users.map((u) => u.userId)).size; + return 20 + uniqueUsers * 3 + 5; +} + +function packResources(resources: RBACResource[], maxHeight: number): RBACResource[][] { + const pages: RBACResource[][] = []; + let current: RBACResource[] = []; + let currentHeight = 0; + + for (const r of resources) { + const h = estimateHeight(r); + if (currentHeight + h > maxHeight && current.length > 0) { + pages.push(current); + current = [r]; + currentHeight = h; + } else { + current.push(r); + currentHeight += h; + } + } + if (current.length > 0) pages.push(current); + return pages; +} + +interface RBACMatrixReportProps { + data: RBACReport; +} + +export default function RBACMatrixReportPage({ data }: RBACMatrixReportProps) { + const header = ; + const footer = ; + const pageProps = { + pageSize: 'a4-landscape' as const, + margins: { top: 1, bottom: 1, left: 5, right: 5 }, + header, + headerHeight: 8, + footer, + footerHeight: 14, + }; + + const resourcePages = packResources(data.resources, 160); + + return ( + <> + + + + + + + + + + + {resourcePages.map((group, pageIdx) => ( + + + +
+ {group.map((resource, idx) => ( + + ))} +
+
+
+ ))} + + {data.changelog.length > 0 && ( + <> + + + + + + )} + + ); +} diff --git a/report/RBACReport.tsx b/report/RBACReport.tsx deleted file mode 100644 index e65c9a9c0..000000000 --- a/report/RBACReport.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { Page, PageBreak } from '@flanksource/facet'; -import type { RBACReport } from './rbac-types.ts'; -import RBACSummarySection from './components/RBACSummarySection.tsx'; -import RBACResourceSection from './components/RBACResourceSection.tsx'; -import RBACChangelogSection from './components/RBACChangelogSection.tsx'; - -function PageHeader({ title }: { title: string }) { - return ( -
- {title} - RBAC Report -
- ); -} - -function PageFooter() { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - return ( -
- Generated {now} -
- ); -} - -function CoverPage({ title, query }: { title: string; query?: string }) { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - return ( -
-
-
- RBAC Report -
-

- {title} -

- {query && ( -
- {query} -
- )} -
-
-
Generated {now}
-
- ); -} - -interface RBACReportProps { - data: RBACReport; -} - -export default function RBACReportPage({ data }: RBACReportProps) { - const header = ; - const footer = ; - const pageProps = { - pageSize: 'a4-landscape' as const, - margins: { top: 1, bottom: 1, left: 0, right: 0 }, - header, - headerHeight: 8, - footer, - footerHeight: 8, - }; - - return ( - <> - - - - - - - - - - - - {data.resources.map((resource, idx) => ( - - - - - - - ))} - - {data.changelog.length > 0 && ( - <> - - - - - - )} - - ); -} diff --git a/report/ViewReport.tsx b/report/ViewReport.tsx index 16286934d..e681ecda8 100644 --- a/report/ViewReport.tsx +++ b/report/ViewReport.tsx @@ -27,7 +27,7 @@ function PageFooter() { ); } -function CoverPage({ data }: { data: ViewReportData }) { +function CoverContent({ data }: { data: ViewReportData }) { const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); @@ -90,7 +90,7 @@ export default function ViewReportPage({ data }: ViewReportProps) { return ( <> - + {viewsList.map((view, idx) => ( diff --git a/report/catalog-report-types.ts b/report/catalog-report-types.ts new file mode 100644 index 000000000..b3ce5828f --- /dev/null +++ b/report/catalog-report-types.ts @@ -0,0 +1,61 @@ +import type { ConfigChange, ConfigAnalysis, ConfigRelationship, ConfigItem } from './config-types.ts'; + +export interface CatalogReportSections { + changes: boolean; + insights: boolean; + relationships: boolean; + access: boolean; + accessLogs: boolean; + configJSON: boolean; +} + +export interface CatalogReportAccess { + userId: string; + userName: string; + email: string; + role: string; + userType: string; + createdAt: string; + lastSignedInAt?: string; + lastReviewedAt?: string; +} + +export interface CatalogReportAccessLog { + userId: string; + userName: string; + configName: string; + configType: string; + createdAt: string; + mfa: boolean; + count: number; + properties?: Record; +} + +export interface CatalogReportData { + title: string; + generatedAt: string; + configItem: ConfigItem & { + config?: string; + name: string; + type?: string; + id: string; + tags?: Record; + labels?: Record; + parent_id?: string; + created_at?: string; + updated_at?: string; + }; + parents: Array<{ + id: string; + name: string; + type?: string; + }>; + sections: CatalogReportSections; + changes: ConfigChange[]; + analyses: ConfigAnalysis[]; + relationships: ConfigRelationship[]; + relatedConfigs: ConfigItem[]; + access: CatalogReportAccess[]; + accessLogs: CatalogReportAccessLog[]; + configJSON?: string; +} diff --git a/report/components/CatalogAccessLogsSection.tsx b/report/components/CatalogAccessLogsSection.tsx new file mode 100644 index 000000000..e574b85da --- /dev/null +++ b/report/components/CatalogAccessLogsSection.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Section, CompactTable } from '@flanksource/facet'; +import type { CatalogReportAccessLog } from '../catalog-report-types.ts'; +import { formatRelative } from './utils.ts'; + +interface Props { + logs: CatalogReportAccessLog[]; +} + +function MFABadge({ mfa }: { mfa: boolean }) { + if (mfa) { + return MFA; + } + return no MFA; +} + +export default function CatalogAccessLogsSection({ logs }: Props) { + if (logs.length === 0) { + return ( +
+

No access log entries found.

+
+ ); + } + + const rows = logs.map((log) => [ + {log.userName}, + log.createdAt ? formatRelative(log.createdAt) : '-', + , + log.count > 1 ? ( + ×{log.count} + ) : ( + '1' + ), + log.properties && Object.keys(log.properties).length > 0 ? ( + + {Object.entries(log.properties).map(([k, v]) => `${k}=${v}`).join(', ')} + + ) : ( + - + ), + ]); + + return ( +
+ +
+ ); +} diff --git a/report/components/CatalogAccessSection.tsx b/report/components/CatalogAccessSection.tsx new file mode 100644 index 000000000..83cf8d9eb --- /dev/null +++ b/report/components/CatalogAccessSection.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Section, CompactTable } from '@flanksource/facet'; +import type { CatalogReportAccess } from '../catalog-report-types.ts'; +import { formatRelative } from './utils.ts'; + +interface Props { + access: CatalogReportAccess[]; +} + +function StaleBadge({ lastSignedInAt }: { lastSignedInAt?: string }) { + if (!lastSignedInAt) { + return never; + } + const days = Math.floor((Date.now() - new Date(lastSignedInAt).getTime()) / 86400000); + if (days > 90) { + return stale; + } + if (days > 30) { + return aging; + } + return null; +} + +export default function CatalogAccessSection({ access }: Props) { + if (access.length === 0) { + return ( +
+

No access entries found.

+
+ ); + } + + const rows = access.map((a) => [ + {a.userName}, + a.role, + {a.email}, + {a.userType}, + a.lastSignedInAt ? ( + + {formatRelative(a.lastSignedInAt)} + + + ) : ( + + - + + + ), + a.lastReviewedAt ? formatRelative(a.lastReviewedAt) : -, + ]); + + return ( +
+ +
+ ); +} diff --git a/report/components/ConfigChangesSection.tsx b/report/components/ConfigChangesSection.tsx new file mode 100644 index 000000000..225a33459 --- /dev/null +++ b/report/components/ConfigChangesSection.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Section, SeverityStatCard } from '@flanksource/facet'; +import { Icon } from '@flanksource/icons/icon'; +import type { ConfigChange, ConfigSeverity } from '../config-types.ts'; +import { formatRelative } from './utils.ts'; + +interface Props { + changes: ConfigChange[]; +} + +const SEVERITY_ORDER: ConfigSeverity[] = ['critical', 'high', 'medium', 'low', 'info']; +const SEVERITY_COLOR: Record = { + critical: 'red', + high: 'orange', + medium: 'yellow', + low: 'blue', + info: 'blue', +}; +const SEVERITY_TEXT: Record = { + critical: 'text-red-700 bg-red-50 border-red-200', + high: 'text-orange-700 bg-orange-50 border-orange-200', + medium: 'text-yellow-700 bg-yellow-50 border-yellow-200', + low: 'text-blue-700 bg-blue-50 border-blue-200', + info: 'text-gray-600 bg-gray-50 border-gray-200', +}; + +function ChangeEntry({ change }: { change: ConfigChange }) { + const sev = change.severity ?? 'info'; + const author = change.createdBy || change.externalCreatedBy || change.source || ''; + return ( +
+ + {change.createdAt ? formatRelative(change.createdAt) : '-'} + + + + + {change.changeType} + {change.summary ?? '-'} + + {sev} + + {author && {author}} + {(change.count ?? 0) > 1 && ( + ×{change.count} + )} +
+ ); +} + +export default function ConfigChangesSection({ changes }: Props) { + const bySeverity = Object.fromEntries( + SEVERITY_ORDER.map((sev) => [sev, changes.filter((c) => (c.severity ?? 'info') === sev).length]) + ); + + return ( +
+
+ {SEVERITY_ORDER.map((sev) => ( + + ))} +
+ {changes.length === 0 ? ( +

No changes recorded.

+ ) : ( +
+ {changes.map((c) => )} +
+ )} +
+ ); +} diff --git a/report/components/ConfigInsightsSection.tsx b/report/components/ConfigInsightsSection.tsx new file mode 100644 index 000000000..ba1ef9818 --- /dev/null +++ b/report/components/ConfigInsightsSection.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { Section, SeverityStatCard } from '@flanksource/facet'; +import { Icon } from '@flanksource/icons/icon'; +import type { ConfigAnalysis, ConfigSeverity, AnalysisType } from '../config-types.ts'; +import { formatDate } from './utils.ts'; + +interface Props { + analyses: ConfigAnalysis[]; +} + +const SEVERITY_ORDER: ConfigSeverity[] = ['critical', 'high', 'medium', 'low', 'info']; +const SEVERITY_COLOR: Record = { + critical: 'red', + high: 'orange', + medium: 'yellow', + low: 'blue', + info: 'blue', +}; +const SEVERITY_TEXT: Record = { + critical: 'text-red-700 bg-red-50 border-red-200', + high: 'text-orange-700 bg-orange-50 border-orange-200', + medium: 'text-yellow-700 bg-yellow-50 border-yellow-200', + low: 'text-blue-700 bg-blue-50 border-blue-200', + info: 'text-gray-600 bg-gray-50 border-gray-200', +}; +const STATUS_TEXT: Record = { + open: 'text-red-700 bg-red-50 border-red-200', + silenced: 'text-yellow-700 bg-yellow-50 border-yellow-200', + resolved: 'text-green-700 bg-green-50 border-green-200', +}; + +const ANALYSIS_TYPES: AnalysisType[] = [ + 'security', 'compliance', 'cost', 'performance', + 'reliability', 'recommendation', 'availability', 'integration', +]; + +function InsightEntry({ analysis }: { analysis: ConfigAnalysis }) { + const sev = analysis.severity ?? 'info'; + return ( +
+ + + + {analysis.analyzer} + {analysis.message || analysis.summary || '-'} + + {sev} + + {analysis.status && ( + + {analysis.status} + + )} + {analysis.lastObserved && ( + {formatDate(analysis.lastObserved)} + )} +
+ ); +} + +function AnalysisTypeGroup({ type, analyses }: { type: string; analyses: ConfigAnalysis[] }) { + if (analyses.length === 0) return null; + + const sorted = [...analyses].sort((a, b) => { + const statusOrder = ['open', 'silenced', 'resolved']; + const statusDiff = statusOrder.indexOf(a.status ?? '') - statusOrder.indexOf(b.status ?? ''); + if (statusDiff !== 0) return statusDiff; + return SEVERITY_ORDER.indexOf(a.severity as ConfigSeverity) - SEVERITY_ORDER.indexOf(b.severity as ConfigSeverity); + }); + + return ( +
+
+ {type} + + {analyses.length} + +
+
+ {sorted.map((a) => )} +
+
+ ); +} + +export default function ConfigInsightsSection({ analyses }: Props) { + const bySeverity = Object.fromEntries( + SEVERITY_ORDER.map((sev) => [sev, analyses.filter((a) => (a.severity ?? 'info') === sev).length]) + ); + const byType = Object.fromEntries( + ANALYSIS_TYPES.map((t) => [t, analyses.filter((a) => a.analysisType === t)]) + ); + + return ( +
+
+ {SEVERITY_ORDER.map((sev) => ( + + ))} +
+ {analyses.length === 0 ? ( +

No analysis results.

+ ) : ( + ANALYSIS_TYPES.map((type) => ( + + )) + )} +
+ ); +} diff --git a/report/components/ConfigItemCard.tsx b/report/components/ConfigItemCard.tsx new file mode 100644 index 000000000..4c2cb9227 --- /dev/null +++ b/report/components/ConfigItemCard.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; +import type { ConfigItem } from '../rbac-types.ts'; +import { formatDate } from './utils.ts'; + +interface Props { + config: ConfigItem; +} + +export default function ConfigItemCard({ config }: Props) { + const tags = { ...config.tags, ...config.labels }; + + return ( +
+
+ {config.type && } + {config.name} + {Object.entries(tags).map(([k, v]) => ( + + {k} + {v || '-'} + + ))} +
+
+ {config.id} + {config.created_at && created: {formatDate(config.created_at)}} + {config.updated_at && updated: {formatDate(config.updated_at)}} +
+
+ ); +} diff --git a/report/components/ConfigLink.tsx b/report/components/ConfigLink.tsx new file mode 100644 index 000000000..6a8ed1f5f --- /dev/null +++ b/report/components/ConfigLink.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; +import { HEALTH_COLORS } from './utils.ts'; +import type { ConfigItem } from '../config-types.ts'; + +interface Props { + config: Pick; + showHealth?: boolean; +} + +export default function ConfigLink({ config, showHealth }: Props) { + return ( + + {config.type && } + {showHealth && config.health && ( + + )} + {config.name} + + ); +} diff --git a/report/components/ConfigRelationshipGraph.tsx b/report/components/ConfigRelationshipGraph.tsx new file mode 100644 index 000000000..6eb8d91c8 --- /dev/null +++ b/report/components/ConfigRelationshipGraph.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Section, CompactTable } from '@flanksource/facet'; +import type { ConfigItem, ConfigRelationship } from '../config-types.ts'; +import { HEALTH_COLORS } from './utils.ts'; +import ConfigLink from './ConfigLink.tsx'; + +interface Props { + centralConfig: ConfigItem; + relationships: ConfigRelationship[]; + relatedConfigs: ConfigItem[]; +} + +function HealthDot({ health }: { health: string }) { + const color = HEALTH_COLORS[health.toLowerCase()] ?? '#6B7280'; + return ( + + + {health} + + ); +} + +function RelationshipGroup({ title, relationships, configLookup }: { + title: string; + relationships: ConfigRelationship[]; + configLookup: Map; +}) { + if (relationships.length === 0) return null; + + const rows = relationships.map((rel) => { + const targetID = rel.direction === 'incoming' ? rel.configID : rel.relatedID; + const config = configLookup.get(targetID); + return [ + config ? : targetID, + config?.type ?? '-', + rel.relation, + config?.health ? : '-', + ]; + }); + + return ( +
+
+ {title} + + {relationships.length} + +
+
+ +
+
+ ); +} + +export default function ConfigRelationshipGraph({ centralConfig, relationships, relatedConfigs }: Props) { + const configLookup = new Map(relatedConfigs.map((c) => [c.id, c])); + + const incoming = relationships.filter((r) => r.direction === 'incoming'); + const outgoing = relationships.filter((r) => r.direction === 'outgoing'); + + return ( +
+
+ + {centralConfig.status && ( + ({centralConfig.status}) + )} +
+ + {relationships.length === 0 ? ( +

No relationships found.

+ ) : ( + <> + + + + )} +
+ ); +} diff --git a/report/components/RBACCoverContent.tsx b/report/components/RBACCoverContent.tsx new file mode 100644 index 000000000..d7ee4389d --- /dev/null +++ b/report/components/RBACCoverContent.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; +import type { RBACReport, ConfigItem } from '../rbac-types.ts'; + +function ConfigBadge({ config }: { config: ConfigItem }) { + return ( +
+ {config.type && } + {config.name} + {config.type} + {config.status && ( + + {config.status} + + )} +
+ ); +} + +function TagBadges({ tags, labels }: { tags?: Record; labels?: Record }) { + const all = { ...tags, ...labels }; + if (Object.keys(all).length === 0) return null; + return ( +
+ {Object.entries(all).map(([k, v]) => ( + + {k} + {v || '-'} + + ))} +
+ ); +} + +interface Props { + report: RBACReport; + subtitle: string; +} + +export default function RBACCoverContent({ report, subtitle }: Props) { + const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); + const { subject, parents } = report; + + return ( +
+
+
+ {subtitle} +
+

+ {report.title} +

+ {report.query && ( +
+ {report.query} +
+ )} +
+ + {subject && ( +
+ {parents && parents.length > 0 && ( +
+ {parents.map((p, i) => ( + + {i > 0 && } + + {p.type && } + {p.name} + + + ))} +
+ )} + + {subject.description && ( +
+ {subject.description} +
+ )} + +
+ )} + +
+ +
+ {report.summary.totalResources} resources · {report.summary.totalUsers} users · {report.summary.staleAccessCount} stale +
+
Generated {now}
+
+ ); +} diff --git a/report/components/RBACMatrixSection.tsx b/report/components/RBACMatrixSection.tsx new file mode 100644 index 000000000..1f60bb1fd --- /dev/null +++ b/report/components/RBACMatrixSection.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; +import type { RBACResource, RBACUserRole } from '../rbac-types.ts'; +import { MatrixTable, Dot } from '@flanksource/facet'; +import { ACCESS_COLORS, STALE_COLORS, ReviewOverdueBadge, ReviewOverdueLegendSwatch, IdentityIcon } from './rbac-visual.tsx'; + +interface Props { + resource: RBACResource; +} + +interface UserRow { + userId: string; + userName: string; + email: string; + roles: Map; +} + +function buildMatrix(resource: RBACResource) { + const roleSet = new Set(); + const userMap = new Map(); + + for (const u of resource.users) { + roleSet.add(u.role); + let row = userMap.get(u.userId); + if (!row) { + row = { userId: u.userId, userName: u.userName, email: u.email, roles: new Map() }; + userMap.set(u.userId, row); + } + row.roles.set(u.role, u); + } + + const roles = [...roleSet].sort(); + const users = [...userMap.values()].sort((a, b) => a.userName.localeCompare(b.userName)); + return { roles, users }; +} + +function loginAgeDays(lastSignedInAt?: string | null): number | null { + if (!lastSignedInAt) return null; + return Math.floor((Date.now() - new Date(lastSignedInAt).getTime()) / 86400000); +} + +function staleColor(lastSignedInAt?: string | null): string | null { + const days = loginAgeDays(lastSignedInAt); + if (days === null || days > 30) return STALE_COLORS.stale30d; + if (days > 7) return STALE_COLORS.stale7d; + return null; +} + +function Indicator({ entry }: { entry?: RBACUserRole }) { + if (!entry) return null; + const indirect = entry.roleSource.startsWith('group:'); + const color = indirect ? ACCESS_COLORS.group : ACCESS_COLORS.direct; + return ( +
+ + {entry.isReviewOverdue && } +
+ ); +} + +export function MatrixLegend() { + return ( +
+ Legend: + + Direct + + + Indirect + + + + Last login > 7d + + + + Last login > 30d + + +
+ ); +} + +export default function RBACMatrixSection({ resource }: Props) { + const { roles, users } = buildMatrix(resource); + if (users.length === 0) return null; + + const matrixRows = users.map((user) => { + const worstStale = [...user.roles.values()].reduce((worst, r) => { + const c = staleColor(r.lastSignedInAt); + if (c === STALE_COLORS.stale30d) return c; + return worst ?? c; + }, null); + const firstRole = [...user.roles.values()][0]; + const roleSource = firstRole?.roleSource; + return { + label: ( + + + {user.userName} + + ), + cells: roles.map((role) => ), + }; + }); + + const tags = { ...resource.tags, ...resource.labels }; + const pathParts = resource.path?.split('.').filter(Boolean) ?? []; + const corner = ( +
+ {pathParts.length > 0 && ( +
+ {pathParts.map((p, i) => ( + + {i > 0 && /} + {p} + + ))} +
+ )} +
+ + {resource.configName} +
+ {Object.keys(tags).length > 0 && ( +
+ {Object.entries(tags).map(([k, v]) => ( + + {k} + {v || '-'} + + ))} +
+ )} +
+ +
+
+ ); + + return ( +
+ +
+ ); +} diff --git a/report/components/RBACResourceSection.tsx b/report/components/RBACResourceSection.tsx deleted file mode 100644 index 4f1aa6341..000000000 --- a/report/components/RBACResourceSection.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import React from 'react'; -import { Section, CompactTable } from '@flanksource/facet'; -import { Icon } from '@flanksource/icons/icon'; -import type { RBACResource, RBACUserRole } from '../rbac-types.ts'; -import { ConfigTypeIcon } from './configTypeIcon.tsx'; - -const ROLE_SOURCE_COLORS: Record = { - direct: { bg: '#DBEAFE', fg: '#1E40AF' }, - group: { bg: '#F3E8FF', fg: '#6B21A8' }, -}; - -const CHANGELOG_TYPE_COLORS: Record = { - PermissionGranted: { bg: '#DCFCE7', fg: '#166534' }, - PermissionRevoked: { bg: '#FEE2E2', fg: '#991B1B' }, - AccessReviewed: { bg: '#DBEAFE', fg: '#1E40AF' }, -}; - -interface Props { - resource: RBACResource; -} - -function fmtDate(iso: string): string { - const d = new Date(iso); - const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - return `${y}-${m}-${day}`; -} - -function fmtDateTime(iso: string): string { - const d = new Date(iso); - const h = String(d.getHours()).padStart(2, '0'); - const min = String(d.getMinutes()).padStart(2, '0'); - return `${fmtDate(iso)}T${h}:${min}`; -} - -function age(iso?: string | null): string { - if (!iso) return 'Never'; - const diff = Date.now() - new Date(iso).getTime(); - const days = Math.floor(diff / 86400000); - if (days < 1) return '<1d'; - if (days < 30) return `${days}d`; - if (days < 365) return `${Math.floor(days / 30)}mo`; - return `${Math.floor(days / 365)}y ${Math.floor((days % 365) / 30)}mo`; -} - -function RoleSourceBadge({ source }: { source: string }) { - const key = source.startsWith('group:') ? 'group' : source; - const colors = ROLE_SOURCE_COLORS[key] || ROLE_SOURCE_COLORS.direct; - return ( - - {source} - - ); -} - -function roleColumn(u: RBACUserRole): React.ReactNode { - return ( - - {u.role} - - - ); -} - -function ReviewAge({ u }: { u: RBACUserRole }) { - const text = age(u.lastReviewedAt); - if (u.isReviewOverdue && text !== 'Never') { - return {text}; - } - if (text === 'Never') { - return Never; - } - return <>{text}; -} - -function LabelBadge({ label, value }: { label: string; value: string }) { - return ( - - - {label} - - - {value} - - - ); -} - -function Pill({ label, color, icon }: { label: string; color?: string; icon?: React.ReactNode }) { - return ( - - {icon} - {label.toUpperCase()} - - ); -} - -function SubHeader({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { - return ( -
- {icon} - {children} -
- ); -} - -function TagsRow({ tags, labels }: { tags?: Record; labels?: Record }) { - const tagKeys = new Set(tags ? Object.keys(tags) : []); - const entries: [string, string][] = []; - if (tags) entries.push(...Object.entries(tags)); - if (labels) { - for (const [k, v] of Object.entries(labels)) { - if (!tagKeys.has(k)) entries.push([k, v]); - } - } - if (entries.length === 0) return null; - - return ( -
- {entries.map(([k, v]) => ( - - ))} -
- ); -} - -function ResourceMeta({ resource }: Props) { - const dateParts: string[] = []; - if (resource.createdAt) dateParts.push(`Created: ${fmtDate(resource.createdAt)}`); - if (resource.updatedAt) dateParts.push(`Updated: ${fmtDate(resource.updatedAt)}`); - - const hasTags = (resource.tags && Object.keys(resource.tags).length > 0) || - (resource.labels && Object.keys(resource.labels).length > 0); - - return ( -
-
- - ID: - - {resource.configId} - - - {dateParts.length > 0 && ( - - {dateParts.join(' \u2022 ')} - - )} -
- - {resource.status && ( -
- -
- )} - - {resource.description && ( -
{resource.description}
- )} - {hasTags && } -
- ); -} - -function ChangeTypeBadge({ type }: { type: string }) { - const colors = CHANGELOG_TYPE_COLORS[type] || { bg: '#E2E8F0', fg: '#334155' }; - return ( - - {type} - - ); -} - -function ChangelogList({ resource }: Props) { - if (!resource.changelog || resource.changelog.length === 0) return null; - - return ( -
- }>Changelog -
- {resource.changelog.map((e, i) => ( -
- {fmtDateTime(e.date)} - - {e.user} - - {e.role} - {e.description && {e.description}} -
- ))} -
-
- ); -} - -function TemporaryAccessTable({ resource }: Props) { - if (!resource.temporaryAccess || resource.temporaryAccess.length === 0) return null; - - const rows = resource.temporaryAccess.map((e) => [ - e.user, - e.role, - e.source, - fmtDateTime(e.grantedAt), - fmtDateTime(e.revokedAt), - e.duration, - ]); - - return ( -
- }>Temporary Access (<72h) - -
- ); -} - -function Legend() { - return ( -
- Role Source: - {Object.entries(ROLE_SOURCE_COLORS).map(([key, colors]) => ( - - - {key} - - ))} - Changelog: - {Object.entries(CHANGELOG_TYPE_COLORS).map(([key, colors]) => ( - - - {key} - - ))} -
- ); -} - -export default function RBACResourceSection({ resource }: Props) { - const rows = resource.users.map((u) => [ - u.userName, - u.email, - roleColumn(u), - fmtDate(u.createdAt), - age(u.lastSignedInAt), - , - ]); - - const title = ( - - - {resource.configName} ({resource.configType}) - - ); - - return ( -
- - }>Users - - - - -
- ); -} diff --git a/report/components/ScraperCard.tsx b/report/components/ScraperCard.tsx new file mode 100644 index 000000000..5d6a12170 --- /dev/null +++ b/report/components/ScraperCard.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; +import type { ScraperInfo } from '../scraper-types.ts'; +import { formatDate } from './utils.ts'; + +const TYPE_ICONS: Record = { + kubernetes: 'Kubernetes', + aws: 'AWS', + azure: 'Azure', + gcp: 'GCP', + file: 'file', + sql: 'database', + http: 'http', + trivy: 'trivy', + terraform: 'Terraform', + githubActions: 'GitHub', + slack: 'Slack', + kubernetesFile: 'Kubernetes', +}; + +interface Props { + scraper: ScraperInfo; +} + +export default function ScraperCard({ scraper }: Props) { + const gitops = scraper.gitops; + const hashShort = scraper.specHash ? scraper.specHash.slice(0, 12) : ''; + + return ( +
+ {/* Header: icons + name */} +
+ {scraper.types.map((t) => ( + + ))} + {scraper.name} + {scraper.source && ( + + {scraper.source} + + )} +
+ + {/* Metadata row */} +
+ {hashShort && ( + + sha256:{hashShort} + + )} + {scraper.createdBy && ( + + {scraper.createdBy} + + )} + {scraper.createdAt && ( + + created {formatDate(scraper.createdAt)} + + )} + {scraper.updatedAt && ( + + updated {formatDate(scraper.updatedAt)} + + )} +
+ + {/* GitOps provenance */} + {gitops && gitops.git.url && ( +
+ + + {gitops.git.url} + + {gitops.git.branch && ( + + {gitops.git.branch} + + )} + {gitops.git.file && ( + + {gitops.git.file} + + )} +
+ )} +
+ ); +} diff --git a/report/components/rbac-visual.tsx b/report/components/rbac-visual.tsx new file mode 100644 index 000000000..be45f21f3 --- /dev/null +++ b/report/components/rbac-visual.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { LuUser, LuUsers, LuServer, LuBot } from 'react-icons/lu'; + +// --- Identity Types --- + +export type IdentityType = 'user' | 'group' | 'service' | 'bot'; + +export interface IdentityInfo { + type: IdentityType; + icon: string; + color: string; + label: string; +} + +const IDENTITY_COLOR = '#64748B'; + +const IDENTITY_ICONS: Record> = { + user: LuUser, + group: LuUsers, + service: LuServer, + bot: LuBot, +}; + +const IDENTITY_LABELS: Record = { + user: 'User', + group: 'Group', + service: 'Service Account', + bot: 'Bot', +}; + +export function identityType(userId: string, roleSource?: string): IdentityInfo { + const resolve = (type: IdentityType): IdentityInfo => ({ + type, icon: type, color: IDENTITY_COLOR, label: IDENTITY_LABELS[type], + }); + if (roleSource?.startsWith('group:')) return resolve('group'); + if (/svc[-_]|service[-_]/i.test(userId)) return resolve('service'); + if (/bot[-_]|automation[-_]|pipeline[-_]/i.test(userId)) return resolve('bot'); + return resolve('user'); +} + +// --- Access Pattern --- + +export const ACCESS_COLORS = { + direct: '#2563EB', + group: '#7C3AED', +}; + +export function isDirect(roleSource: string): boolean { + return !roleSource.startsWith('group:'); +} + +// --- Staleness --- + +export const STALE_COLORS = { + stale7d: '#EAB308', + stale30d: '#DC2626', +}; + +// --- Review Status --- + +export const REVIEW_OVERDUE_COLOR = '#DC2626'; + +export function ReviewOverdueBadge() { + return ( +
+ ); +} + +export function ReviewOverdueLegendSwatch() { + return ( + + + + + Review Overdue + + ); +} + +// --- Reusable Visual Components --- + +export function IdentityIcon({ userId, roleSource, size = 14 }: { userId: string; roleSource?: string; size?: number }) { + const info = identityType(userId, roleSource); + const IconComponent = IDENTITY_ICONS[info.type]; + return ; +} + +export function AccessIndicator({ direct, color, size = 2.5 }: { direct: boolean; color: string; size?: number }) { + return ( +
+ ); +} diff --git a/report/config-types.ts b/report/config-types.ts new file mode 100644 index 000000000..f29bdfa71 --- /dev/null +++ b/report/config-types.ts @@ -0,0 +1,63 @@ +export type ConfigSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical'; +export type ConfigHealth = 'healthy' | 'warning' | 'unhealthy' | 'unknown'; +export type AnalysisType = + | 'security' | 'compliance' | 'cost' | 'performance' + | 'reliability' | 'recommendation' | 'integration' | 'availability'; + +export interface ConfigItem { + id: string; + name: string; + type?: string; + configClass?: string; + status?: string; + health?: ConfigHealth; + description?: string; + labels?: Record; + tags?: Record; + costTotal30d?: number; + createdAt?: string; + updatedAt?: string; +} + +export interface ConfigChange { + id?: string; + configID?: string; + changeType: string; + severity?: ConfigSeverity; + source?: string; + summary?: string; + createdBy?: string; + externalCreatedBy?: string; + createdAt?: string; + firstObserved?: string; + count?: number; +} + +export interface ConfigAnalysis { + id?: string; + configID?: string; + analyzer: string; + message?: string; + summary?: string; + status?: string; + severity?: ConfigSeverity; + analysisType?: AnalysisType; + source?: string; + firstObserved?: string; + lastObserved?: string; +} + +export interface ConfigRelationship { + configID: string; + relatedID: string; + relation: string; + direction?: 'incoming' | 'outgoing'; +} + +export interface ConfigReportData { + configItem: ConfigItem; + changes: ConfigChange[]; + analyses: ConfigAnalysis[]; + relationships: ConfigRelationship[]; + relatedConfigs: ConfigItem[]; +} diff --git a/report/kitchen-sink-data.ts b/report/kitchen-sink-data.ts new file mode 100644 index 000000000..d4cbab948 --- /dev/null +++ b/report/kitchen-sink-data.ts @@ -0,0 +1,11 @@ +import { readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import yaml from 'js-yaml'; +import type { ConfigReportData } from './config-types.ts'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const raw = readFileSync(resolve(__dirname, 'testdata/kitchen-sink.yaml'), 'utf-8'); +const data = yaml.load(raw) as ConfigReportData; + +export default data; diff --git a/report/package-lock.json b/report/package-lock.json index f4df4ce9a..c98598b95 100644 --- a/report/package-lock.json +++ b/report/package-lock.json @@ -8,7 +8,7 @@ "name": "application-report", "version": "1.0.0", "dependencies": { - "@flanksource/facet": "^0.1.30", + "@flanksource/facet": "file:/Users/moshe/go/src/github.com/flanksource/facet", "@flanksource/icons": "^1.0.53", "js-yaml": "^4.1.0" }, @@ -20,36 +20,67 @@ "vite": ">=7.3.2" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", + "../../../facet": { + "extraneous": true + }, + "../../facet": { + "name": "@flanksource/facet", + "version": "0.1.27", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@flanksource/icons": "^1.0.53", + "@iconify/react": "^5.1.0", + "@xyflow/react": "^12.0.0", + "clsx": "^2.1.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "dagre": "^0.8.5", + "dayjs": "^1.11.13", + "pdf-lib": "^1.17.1", + "react-icons": "^5.4.0", + "shiki": "^1.0.0" + }, + "devDependencies": { + "@mdx-js/rollup": "^3.0.0", + "@storybook/addon-docs": "^8.6.14", + "@storybook/addon-essentials": "^8.6.14", + "@storybook/react": "^8.6.14", + "@storybook/react-vite": "^8.6.14", + "@tailwindcss/postcss": "^4.1.17", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.17", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.8", + "@types/dagre": "^0.7.52", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "baseline-browser-mapping": "^2.10.0", + "chalk": "^5.6.2", + "chromatic": "^16.0.0", + "juice": "^11.0.3", + "ora": "^8.2.0", + "puppeteer": "^24.26.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "remark-gfm": "^4.0.0", + "storybook": "^8.6.14", + "tailwindcss": "^3.4.3", + "tsx": "^4.20.6", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "vite-plugin-dts": "^4.5.0", + "vitest": "^4.1.1" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node": ">=20.19" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/@esbuild/aix-ppc64": { @@ -495,30 +526,8 @@ } }, "node_modules/@flanksource/facet": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@flanksource/facet/-/facet-0.1.30.tgz", - "integrity": "sha512-9BvGI70uv65Xen6Kh5SXfzMLDGEe3yjLutrVpkldSAJWEhGcz7IF7oveQ+MrWmAAUlQwanzJ45aNsBgSAsQvjQ==", - "dependencies": { - "@flanksource/icons": "^1.0.41", - "@iconify/react": "^5.1.0", - "@testing-library/react": "^16.3.2", - "@xyflow/react": "^12.0.0", - "clsx": "^2.1.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.2.0", - "dagre": "^0.8.5", - "dayjs": "^1.11.13", - "pdf-lib": "^1.17.1", - "react-icons": "^5.4.0", - "shiki": "^1.0.0" - }, - "engines": { - "node": ">=20.19" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } + "resolved": "../../facet", + "link": true }, "node_modules/@flanksource/icons": { "version": "1.0.53", @@ -529,225 +538,6 @@ "react": "*" } }, - "node_modules/@iconify/react": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@iconify/react/-/react-5.2.1.tgz", - "integrity": "sha512-37GDR3fYDZmnmUn9RagyaX+zca24jfVOMY8E1IXTqJuE8pxNtN51KWPQe3VODOWvuUurq7q9uUu3CFrpqj5Iqg==", - "license": "MIT", - "dependencies": { - "@iconify/types": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/cyberalien" - }, - "peerDependencies": { - "react": ">=16" - } - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@pdf-lib/standard-fonts": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", - "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", - "license": "MIT", - "dependencies": { - "pako": "^1.0.6" - } - }, - "node_modules/@pdf-lib/upng": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", - "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", - "license": "MIT", - "dependencies": { - "pako": "^1.0.10" - } - }, - "node_modules/@shikijs/core": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", - "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", - "license": "MIT", - "dependencies": { - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.4" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", - "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "oniguruma-to-es": "^2.2.0" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", - "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1" - } - }, - "node_modules/@shikijs/langs": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", - "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2" - } - }, - "node_modules/@shikijs/themes": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", - "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "1.29.2" - } - }, - "node_modules/@shikijs/types": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", - "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -755,15 +545,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/node": { "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", @@ -774,379 +555,12 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@xyflow/react": { - "version": "12.10.1", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz", - "integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==", - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.75", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.75", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz", - "integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==", - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "license": "MIT", - "dependencies": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "license": "MIT" - }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -1217,70 +631,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1299,12 +649,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1535,60 +879,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" - }, - "node_modules/regex": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", - "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", - "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", - "license": "MIT", - "dependencies": { - "regex": "^5.1.1", - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "license": "MIT" - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", diff --git a/report/package.json b/report/package.json index d41e48179..3a305d2c2 100644 --- a/report/package.json +++ b/report/package.json @@ -9,7 +9,7 @@ "mission-control": "npm run pdf" }, "dependencies": { - "@flanksource/facet": "^0.1.30", + "@flanksource/facet": "file:/Users/moshe/go/src/github.com/flanksource/facet", "@flanksource/icons": "^1.0.53", "js-yaml": "^4.1.0" }, diff --git a/report/rbac-types.ts b/report/rbac-types.ts index 56066d8b2..dc42a26cc 100644 --- a/report/rbac-types.ts +++ b/report/rbac-types.ts @@ -16,6 +16,9 @@ export interface RBACResource { configId: string; configName: string; configType: string; + configClass?: string; + parentId?: string; + path?: string; status?: string; health?: string; description?: string; @@ -62,6 +65,8 @@ export interface RBACUserResource { configId: string; configName: string; configType: string; + configClass?: string; + path?: string; role: string; roleSource: string; createdAt: string; @@ -84,10 +89,28 @@ export interface RBACUserReport { resources: RBACUserResource[]; } +export interface ConfigItem { + id: string; + name?: string; + type?: string; + config_class?: string; + status?: string; + health?: string; + description?: string; + path?: string; + parent_id?: string; + tags?: Record; + labels?: Record; + created_at?: string; + updated_at?: string; +} + export interface RBACReport { title: string; query?: string; generatedAt: string; + subject?: ConfigItem; + parents?: ConfigItem[]; resources: RBACResource[]; changelog: RBACChangeEntry[]; summary: RBACSummary; diff --git a/report/scraper-types.ts b/report/scraper-types.ts new file mode 100644 index 000000000..ee6e4a77b --- /dev/null +++ b/report/scraper-types.ts @@ -0,0 +1,27 @@ +export interface GitOpsSource { + git: { + url: string; + branch: string; + file: string; + dir: string; + link: string; + }; + kustomize: { + path: string; + file: string; + }; +} + +export interface ScraperInfo { + id: string; + name: string; + namespace?: string; + description?: string; + source?: string; + types: string[]; + specHash: string; + createdBy?: string; + createdAt: string; + updatedAt?: string; + gitops?: GitOpsSource; +} diff --git a/report/testdata/kitchen-sink.yaml b/report/testdata/kitchen-sink.yaml new file mode 100644 index 000000000..8b67ae563 --- /dev/null +++ b/report/testdata/kitchen-sink.yaml @@ -0,0 +1,393 @@ +configItem: + id: "cfg-eks-001" + name: "prod-eks-cluster" + type: "AWS::EKS::Cluster" + configClass: "Cluster" + status: "Active" + health: "healthy" + description: "Production EKS cluster running Mission Control workloads in us-east-1" + labels: + env: "production" + team: "platform" + region: "us-east-1" + costTotal30d: 4280.50 + createdAt: "2025-03-15T09:00:00Z" + updatedAt: "2026-03-28T12:00:00Z" + +changes: + - id: "chg-001" + configID: "cfg-eks-001" + changeType: "diff" + severity: "info" + source: "kubernetes" + summary: "Node pool autoscaler adjusted desired count from 3 to 5" + createdBy: "cluster-autoscaler" + createdAt: "2026-03-30T08:15:00Z" + count: 1 + + - id: "chg-002" + configID: "cfg-eks-001" + changeType: "Pulled" + severity: "info" + source: "kubernetes" + summary: "Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42" + createdAt: "2026-03-30T07:30:00Z" + count: 3 + + - id: "chg-003" + configID: "cfg-eks-001" + changeType: "ScalingReplicaSet" + severity: "low" + source: "kubernetes" + summary: "Deployment incident-commander scaled from 2 to 3 replicas" + externalCreatedBy: "hpa-controller" + createdAt: "2026-03-29T22:00:00Z" + + - id: "chg-004" + configID: "cfg-eks-001" + changeType: "diff" + severity: "medium" + source: "terraform" + summary: "EKS cluster version upgraded from 1.28 to 1.29" + createdBy: "alice@flanksource.com" + createdAt: "2026-03-29T14:00:00Z" + + - id: "chg-005" + configID: "cfg-eks-001" + changeType: "PolicyUpdate" + severity: "high" + source: "argocd" + summary: "Network policy updated: restricted egress to 10.0.0.0/8 for namespace mc" + createdBy: "bob@flanksource.com" + createdAt: "2026-03-28T16:00:00Z" + + - id: "chg-006" + configID: "cfg-eks-001" + changeType: "diff" + severity: "critical" + source: "aws-config" + summary: "IAM role policy detached: eks-admin-access removed from cluster role" + createdBy: "security-automation" + createdAt: "2026-03-28T10:00:00Z" + + - id: "chg-007" + configID: "cfg-eks-001" + changeType: "FieldsV1" + severity: "info" + source: "kubernetes" + summary: "ConfigMap kube-proxy updated with new CIDR ranges" + createdAt: "2026-03-27T18:00:00Z" + count: 2 + + - id: "chg-008" + configID: "cfg-eks-001" + changeType: "diff" + severity: "low" + source: "terraform" + summary: "Added tag cost-center=platform-engineering to cluster" + createdBy: "carol@flanksource.com" + createdAt: "2026-03-27T09:00:00Z" + + - id: "chg-009" + configID: "cfg-eks-001" + changeType: "ScalingReplicaSet" + severity: "info" + source: "kubernetes" + summary: "Deployment canary-checker scaled from 1 to 2 replicas" + externalCreatedBy: "hpa-controller" + createdAt: "2026-03-26T20:00:00Z" + + - id: "chg-010" + configID: "cfg-eks-001" + changeType: "diff" + severity: "medium" + source: "argocd" + summary: "Helm release cert-manager upgraded from v1.13.3 to v1.14.1" + createdBy: "alice@flanksource.com" + createdAt: "2026-03-26T11:00:00Z" + + - id: "chg-011" + configID: "cfg-eks-001" + changeType: "Pulled" + severity: "info" + source: "kubernetes" + summary: "Image flanksource/canary-checker:v1.0.350 pulled" + createdAt: "2026-03-25T15:00:00Z" + count: 5 + + - id: "chg-012" + configID: "cfg-eks-001" + changeType: "PolicyUpdate" + severity: "high" + source: "aws-config" + summary: "Security group sg-0abc123 ingress rule added: allow 443 from 0.0.0.0/0" + createdBy: "terraform" + createdAt: "2026-03-25T10:00:00Z" + + - id: "chg-013" + configID: "cfg-eks-001" + changeType: "diff" + severity: "low" + source: "kubernetes" + summary: "PodDisruptionBudget added for incident-commander (minAvailable: 2)" + createdBy: "bob@flanksource.com" + createdAt: "2026-03-24T14:00:00Z" + +analyses: + - id: "ana-001" + configID: "cfg-eks-001" + analyzer: "Trivy" + message: "Container image flanksource/incident-commander:v1.4.200 has 3 high CVEs (CVE-2026-1234, CVE-2026-1235, CVE-2026-1236)" + status: "open" + severity: "high" + analysisType: "security" + source: "trivy-operator" + firstObserved: "2026-03-28T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-002" + configID: "cfg-eks-001" + analyzer: "Trivy" + message: "Base image golang:1.23-alpine has known vulnerability in libcrypto (CVE-2026-0891)" + status: "open" + severity: "critical" + analysisType: "security" + source: "trivy-operator" + firstObserved: "2026-03-25T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-003" + configID: "cfg-eks-001" + analyzer: "OPA/Gatekeeper" + message: "Pod incident-commander-7f8b9c running as root user in namespace mc" + status: "open" + severity: "medium" + analysisType: "compliance" + source: "gatekeeper" + firstObserved: "2026-03-20T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-004" + configID: "cfg-eks-001" + analyzer: "OPA/Gatekeeper" + message: "Namespace mc missing required label: data-classification" + status: "silenced" + severity: "low" + analysisType: "compliance" + source: "gatekeeper" + firstObserved: "2026-03-15T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-005" + configID: "cfg-eks-001" + analyzer: "AWS Cost Optimizer" + message: "EKS node group i3.xlarge instances are underutilized (avg CPU 18%). Consider downsizing to i3.large." + status: "open" + severity: "medium" + analysisType: "cost" + source: "aws-cost-explorer" + firstObserved: "2026-03-01T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-006" + configID: "cfg-eks-001" + analyzer: "AWS Cost Optimizer" + message: "NAT Gateway data processing charges are 40% above baseline ($320/mo). Review egress traffic patterns." + status: "open" + severity: "low" + analysisType: "cost" + source: "aws-cost-explorer" + firstObserved: "2026-03-10T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-007" + configID: "cfg-eks-001" + analyzer: "Prometheus Advisor" + message: "P99 API response latency exceeded 500ms threshold 12 times in the last 7 days" + status: "open" + severity: "high" + analysisType: "performance" + source: "prometheus" + firstObserved: "2026-03-23T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-008" + configID: "cfg-eks-001" + analyzer: "AWS Best Practices" + message: "EKS cluster running version 1.29 — version 1.30 is available with security patches" + status: "open" + severity: "info" + analysisType: "recommendation" + source: "aws-advisor" + firstObserved: "2026-03-28T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-009" + configID: "cfg-eks-001" + analyzer: "AWS Best Practices" + message: "Enable EKS control plane logging for audit, authenticator, and scheduler components" + status: "resolved" + severity: "medium" + analysisType: "reliability" + source: "aws-advisor" + firstObserved: "2026-02-15T09:00:00Z" + lastObserved: "2026-03-20T09:00:00Z" + + - id: "ana-010" + configID: "cfg-eks-001" + analyzer: "Prometheus Advisor" + message: "Node ip-10-0-2-18 memory utilization consistently above 85% — risk of OOM kills" + status: "open" + severity: "high" + analysisType: "reliability" + source: "prometheus" + firstObserved: "2026-03-26T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + + - id: "ana-011" + configID: "cfg-eks-001" + analyzer: "Trivy" + message: "Resolved: CVE-2025-9999 in nginx ingress controller patched in v1.10.1" + status: "resolved" + severity: "high" + analysisType: "security" + source: "trivy-operator" + firstObserved: "2026-02-01T09:00:00Z" + lastObserved: "2026-03-15T09:00:00Z" + +relationships: + - configID: "cfg-eks-001" + relatedID: "cfg-vpc-001" + relation: "RunsIn" + direction: "outgoing" + + - configID: "cfg-eks-001" + relatedID: "cfg-iam-001" + relation: "ManagedBy" + direction: "outgoing" + + - configID: "cfg-eks-001" + relatedID: "cfg-sg-001" + relation: "DependsOn" + direction: "outgoing" + + - configID: "cfg-eks-001" + relatedID: "cfg-rds-001" + relation: "DependsOn" + direction: "outgoing" + + - configID: "cfg-deploy-001" + relatedID: "cfg-eks-001" + relation: "RunsOn" + direction: "incoming" + + - configID: "cfg-deploy-002" + relatedID: "cfg-eks-001" + relation: "RunsOn" + direction: "incoming" + + - configID: "cfg-deploy-003" + relatedID: "cfg-eks-001" + relation: "RunsOn" + direction: "incoming" + + - configID: "cfg-ns-001" + relatedID: "cfg-eks-001" + relation: "ChildOf" + direction: "incoming" + + - configID: "cfg-node-001" + relatedID: "cfg-eks-001" + relation: "ChildOf" + direction: "incoming" + + - configID: "cfg-node-002" + relatedID: "cfg-eks-001" + relation: "ChildOf" + direction: "incoming" + +relatedConfigs: + - id: "cfg-vpc-001" + name: "prod-vpc" + type: "AWS::EC2::VPC" + configClass: "Network" + status: "available" + health: "healthy" + labels: + env: "production" + + - id: "cfg-iam-001" + name: "eks-cluster-role" + type: "AWS::IAM::Role" + configClass: "IAM" + status: "active" + health: "healthy" + + - id: "cfg-sg-001" + name: "eks-cluster-sg" + type: "AWS::EC2::SecurityGroup" + configClass: "Network" + status: "active" + health: "warning" + labels: + env: "production" + + - id: "cfg-rds-001" + name: "mission-control-db" + type: "AWS::RDS::Instance" + configClass: "Database" + status: "available" + health: "healthy" + labels: + env: "production" + engine: "postgresql" + + - id: "cfg-deploy-001" + name: "incident-commander" + type: "Kubernetes::Deployment" + configClass: "Deployment" + status: "Running" + health: "healthy" + labels: + app: "incident-commander" + + - id: "cfg-deploy-002" + name: "canary-checker" + type: "Kubernetes::Deployment" + configClass: "Deployment" + status: "Running" + health: "healthy" + labels: + app: "canary-checker" + + - id: "cfg-deploy-003" + name: "config-db" + type: "Kubernetes::Deployment" + configClass: "Deployment" + status: "Running" + health: "unhealthy" + labels: + app: "config-db" + + - id: "cfg-ns-001" + name: "mc" + type: "Kubernetes::Namespace" + configClass: "Namespace" + status: "Active" + health: "healthy" + + - id: "cfg-node-001" + name: "ip-10-0-1-42" + type: "Kubernetes::Node" + configClass: "Node" + status: "Ready" + health: "healthy" + + - id: "cfg-node-002" + name: "ip-10-0-2-18" + type: "Kubernetes::Node" + configClass: "Node" + status: "Ready" + health: "warning" + labels: + instance-type: "i3.xlarge" From 681c32e17771982ad6625cd736cf3f8670b6b2e3 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 5 Apr 2026 17:34:56 +0300 Subject: [PATCH 25/48] fix(api): remove premature defer stop calls and improve resource cleanup Replace defer stop() with shutdown hook registration to ensure proper cleanup ordering. Add non-blocking sends to avoid goroutine hangs. Support array-type JWT audience claims and persist session storage. --- cmd/connection_browser.go | 16 ++++++++++++---- cmd/connection_test_cmd.go | 1 - connection/jwt.go | 4 ++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cmd/connection_browser.go b/cmd/connection_browser.go index 05eb9131a..50c447e83 100644 --- a/cmd/connection_browser.go +++ b/cmd/connection_browser.go @@ -243,12 +243,15 @@ func launchBrowserAndCapture(ctx gocontext.Context, flags browserLoginFlags) (*b } func waitForLoginComplete(browserCtx gocontext.Context, flags browserLoginFlags) { - doneCh := make(chan struct{}, 1) + doneCh := make(chan struct{}, 3) go func() { reader := bufio.NewReader(os.Stdin) _, _ = reader.ReadString('\n') - doneCh <- struct{}{} + select { + case doneCh <- struct{}{}: + default: + } }() if flags.WaitForURL != "" { @@ -375,7 +378,6 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe return err } shutdown.AddHookWithPriority("database", shutdown.PriorityCritical, stop) - defer stop() props := make(map[string]string) @@ -402,6 +404,13 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe } props["storageState"] = string(storageJSON) + if len(data.SessionStorage) > 0 { + sessionJSON, err := json.Marshal(data.SessionStorage) + if err == nil { + props["sessionStorage"] = string(sessionJSON) + } + } + // Also store cookies as headers for HTTP connection compatibility if len(data.Cookies) > 0 { parts := make([]string, len(data.Cookies)) @@ -492,7 +501,6 @@ func runBrowserTest(cmd *cobra.Command, args []string) error { return err } shutdown.AddHookWithPriority("database", shutdown.PriorityCritical, stop) - defer stop() verbose := clicky.Flags.LevelCount diff --git a/cmd/connection_test_cmd.go b/cmd/connection_test_cmd.go index 3592506ab..81d915a6c 100644 --- a/cmd/connection_test_cmd.go +++ b/cmd/connection_test_cmd.go @@ -72,7 +72,6 @@ func runConnectionTestFromDB(name, namespace string, overrides *connectionFlags) return nil, err } shutdown.AddHookWithPriority("database", shutdown.PriorityCritical, stop) - defer stop() var conn models.Connection if err := ctx.DB().Where("name = ? AND namespace = ? AND deleted_at IS NULL", name, namespace).First(&conn).Error; err != nil { diff --git a/connection/jwt.go b/connection/jwt.go index 308ea502f..adec57995 100644 --- a/connection/jwt.go +++ b/connection/jwt.go @@ -85,6 +85,10 @@ func DecodeJWT(token string) *JWT { j := &JWT{Raw: token} if v, ok := claims["aud"].(string); ok { j.Audience = v + } else if arr, ok := claims["aud"].([]any); ok && len(arr) > 0 { + if s, ok := arr[0].(string); ok { + j.Audience = s + } } if v, ok := claims["sub"].(string); ok { j.Subject = v From b618a65c1c90c91ad8333d1fd89a9b3550c2436a Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 6 Apr 2026 21:10:19 +0300 Subject: [PATCH 26/48] feat(report): show date range on catalog report cover page Add from/to fields to CatalogReport struct, populated from the resolved sinceTime in BuildReport. Display the date range on the cover page. --- api/catalog_report.go | 289 ++++++++++++++++++++ catalog_report/report.go | 485 +++++++++++++++++++++++++++++++++ catalog_report/report_test.go | 69 +++++ report/CatalogReport.tsx | 224 +++++++++++++-- report/catalog-report-types.ts | 43 +++ 5 files changed, 1080 insertions(+), 30 deletions(-) create mode 100644 api/catalog_report.go create mode 100644 catalog_report/report.go create mode 100644 catalog_report/report_test.go diff --git a/api/catalog_report.go b/api/catalog_report.go new file mode 100644 index 000000000..1ab4f4ebd --- /dev/null +++ b/api/catalog_report.go @@ -0,0 +1,289 @@ +package api + +import ( + "fmt" + "time" + + "github.com/flanksource/duty/models" +) + +func ConfigPermalink(configID string) string { + if FrontendURL == "" { + return "" + } + return fmt.Sprintf("%s/catalog/%s", FrontendURL, configID) +} + +type CatalogReport struct { + Title string `json:"title"` + GeneratedAt time.Time `json:"generatedAt"` + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` + Sections CatalogReportSections `json:"sections"` + Recursive bool `json:"recursive,omitempty"` + GroupBy string `json:"groupBy,omitempty"` + Entries []CatalogReportEntry `json:"entries"` + + // Deprecated: use Entries[0] for single-config reports + ConfigItem models.ConfigItem `json:"configItem"` + Parents []models.ConfigItem `json:"parents"` + + Changes []CatalogReportChange `json:"changes,omitempty"` + Analyses []CatalogReportAnalysis `json:"analyses,omitempty"` + Relationships []CatalogReportRelationship `json:"relationships,omitempty"` + RelatedConfigs []CatalogReportConfigItem `json:"relatedConfigs,omitempty"` + RelationshipTree *CatalogReportTreeNode `json:"relationshipTree,omitempty"` + Access []CatalogReportAccess `json:"access,omitempty"` + AccessLogs []CatalogReportAccessLog `json:"accessLogs,omitempty"` + ConfigJSON *string `json:"configJSON,omitempty"` + ConfigGroups []CatalogReportConfigGroup `json:"configGroups,omitempty"` +} + +type CatalogReportEntry struct { + ConfigItem CatalogReportConfigItem `json:"configItem"` + Parents []CatalogReportConfigItem `json:"parents,omitempty"` + RelationshipTree *CatalogReportTreeNode `json:"relationshipTree,omitempty"` + ChangeCount int `json:"changeCount"` + InsightCount int `json:"insightCount"` + AccessCount int `json:"accessCount"` + RBACResources []RBACResource `json:"rbacResources,omitempty"` + Changes []CatalogReportChange `json:"changes,omitempty"` + Analyses []CatalogReportAnalysis `json:"analyses,omitempty"` + Access []CatalogReportAccess `json:"access,omitempty"` + AccessLogs []CatalogReportAccessLog `json:"accessLogs,omitempty"` +} + +type CatalogReportConfigGroup struct { + ConfigItem CatalogReportConfigItem `json:"configItem"` + Changes []CatalogReportChange `json:"changes,omitempty"` + Analyses []CatalogReportAnalysis `json:"analyses,omitempty"` + Access []CatalogReportAccess `json:"access,omitempty"` + AccessLogs []CatalogReportAccessLog `json:"accessLogs,omitempty"` +} + +type CatalogReportSections struct { + Changes bool `json:"changes"` + Insights bool `json:"insights"` + Relationships bool `json:"relationships"` + Access bool `json:"access"` + AccessLogs bool `json:"accessLogs"` + ConfigJSON bool `json:"configJSON"` +} + +// CatalogReportChange wraps models.ConfigChange with camelCase JSON tags +// to match report/config-types.ts ConfigChange interface. +type CatalogReportArtifact struct { + ID string `json:"id"` + Filename string `json:"filename"` + ContentType string `json:"contentType"` + Size int64 `json:"size"` + DataURI string `json:"dataUri,omitempty"` +} + +type CatalogReportChange struct { + ID string `json:"id,omitempty"` + ConfigID string `json:"configID,omitempty"` + ConfigName string `json:"configName,omitempty"` + ConfigType string `json:"configType,omitempty"` + Permalink string `json:"permalink,omitempty"` + ChangeType string `json:"changeType"` + Severity string `json:"severity,omitempty"` + Source string `json:"source,omitempty"` + Summary string `json:"summary,omitempty"` + CreatedBy string `json:"createdBy,omitempty"` + ExternalCreatedBy string `json:"externalCreatedBy,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + Count int `json:"count,omitempty"` + Artifacts []CatalogReportArtifact `json:"artifacts,omitempty"` +} + +func NewCatalogReportChange(c models.ConfigChange, configName, configType string) CatalogReportChange { + r := CatalogReportChange{ + ID: c.ID, + ConfigID: c.ConfigID, + ConfigName: configName, + ConfigType: configType, + Permalink: ConfigPermalink(c.ConfigID), + ChangeType: c.ChangeType, + Severity: string(c.Severity), + Source: c.Source, + Summary: c.Summary, + CreatedAt: c.CreatedAt.Format(time.RFC3339), + Count: c.Count, + } + if c.CreatedBy != nil { + r.CreatedBy = c.CreatedBy.String() + } + if c.ExternalCreatedBy != nil { + r.ExternalCreatedBy = *c.ExternalCreatedBy + } + return r +} + +type CatalogReportAnalysis struct { + ID string `json:"id,omitempty"` + ConfigID string `json:"configID,omitempty"` + ConfigName string `json:"configName,omitempty"` + ConfigType string `json:"configType,omitempty"` + Permalink string `json:"permalink,omitempty"` + Analyzer string `json:"analyzer"` + Message string `json:"message,omitempty"` + Summary string `json:"summary,omitempty"` + Status string `json:"status,omitempty"` + Severity string `json:"severity,omitempty"` + AnalysisType string `json:"analysisType,omitempty"` + Source string `json:"source,omitempty"` + FirstObserved string `json:"firstObserved,omitempty"` + LastObserved string `json:"lastObserved,omitempty"` +} + +func NewCatalogReportAnalysis(a models.ConfigAnalysis, configName, configType string) CatalogReportAnalysis { + r := CatalogReportAnalysis{ + ID: a.ID.String(), + ConfigID: a.ConfigID.String(), + ConfigName: configName, + ConfigType: configType, + Permalink: ConfigPermalink(a.ConfigID.String()), + Analyzer: a.Analyzer, + Message: a.Message, + Summary: a.Summary, + Status: a.Status, + Severity: string(a.Severity), + AnalysisType: string(a.AnalysisType), + Source: a.Source, + } + if a.FirstObserved != nil { + r.FirstObserved = a.FirstObserved.Format(time.RFC3339) + } + if a.LastObserved != nil { + r.LastObserved = a.LastObserved.Format(time.RFC3339) + } + return r +} + +type CatalogReportRelationship struct { + ConfigID string `json:"configID"` + RelatedID string `json:"relatedID"` + Relation string `json:"relation"` + Direction string `json:"direction,omitempty"` +} + +type CatalogReportTreeNode struct { + CatalogReportConfigItem `json:",inline"` + EdgeType string `json:"edgeType,omitempty"` // "parent", "child", "related", "target" + Relation string `json:"relation,omitempty"` + Children []CatalogReportTreeNode `json:"children,omitempty"` +} + +type CatalogReportConfigItem struct { + ID string `json:"id"` + Permalink string `json:"permalink,omitempty"` + Name string `json:"name"` + Type string `json:"type,omitempty"` + ConfigClass string `json:"configClass,omitempty"` + Status string `json:"status,omitempty"` + Health string `json:"health,omitempty"` + Description string `json:"description,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` +} + +func NewCatalogReportConfigItem(ci models.ConfigItem) CatalogReportConfigItem { + r := CatalogReportConfigItem{ + ID: ci.ID.String(), + Permalink: ConfigPermalink(ci.ID.String()), + Name: ci.GetName(), + ConfigClass: ci.ConfigClass, + Tags: ci.Tags, + } + if ci.Type != nil { + r.Type = *ci.Type + } + if ci.Status != nil { + r.Status = *ci.Status + } + if ci.Health != nil { + r.Health = string(*ci.Health) + } + if ci.Description != nil { + r.Description = *ci.Description + } + if ci.Labels != nil { + r.Labels = *ci.Labels + } + if !ci.CreatedAt.IsZero() { + r.CreatedAt = ci.CreatedAt.Format(time.RFC3339) + } + if ci.UpdatedAt != nil { + r.UpdatedAt = ci.UpdatedAt.Format(time.RFC3339) + } + return r +} + +type CatalogReportAccess struct { + ConfigID string `json:"configId,omitempty"` + ConfigName string `json:"configName,omitempty"` + ConfigType string `json:"configType,omitempty"` + Permalink string `json:"permalink,omitempty"` + UserID string `json:"userId"` + UserName string `json:"userName"` + Email string `json:"email"` + Role string `json:"role"` + UserType string `json:"userType"` + CreatedAt string `json:"createdAt"` + LastSignedInAt *string `json:"lastSignedInAt,omitempty"` + LastReviewedAt *string `json:"lastReviewedAt,omitempty"` +} + +func NewCatalogReportAccess(a models.ConfigAccessSummary) CatalogReportAccess { + r := CatalogReportAccess{ + ConfigID: a.ConfigID.String(), + ConfigName: a.ConfigName, + ConfigType: a.ConfigType, + Permalink: ConfigPermalink(a.ConfigID.String()), + UserID: a.ExternalUserID.String(), + UserName: a.User, + Email: a.Email, + Role: a.Role, + UserType: a.UserType, + CreatedAt: a.CreatedAt.Format(time.RFC3339), + } + if a.LastSignedInAt != nil { + s := a.LastSignedInAt.Format(time.RFC3339) + r.LastSignedInAt = &s + } + if a.LastReviewedAt != nil { + s := a.LastReviewedAt.Format(time.RFC3339) + r.LastReviewedAt = &s + } + return r +} + +type CatalogReportAccessLog struct { + ConfigID string `json:"configId,omitempty"` + Permalink string `json:"permalink,omitempty"` + UserID string `json:"userId"` + UserName string `json:"userName"` + ConfigName string `json:"configName"` + ConfigType string `json:"configType"` + CreatedAt string `json:"createdAt"` + MFA bool `json:"mfa"` + Count int `json:"count"` + Properties map[string]string `json:"properties,omitempty"` +} + +func NewCatalogReportRelationship(configID string, rc models.ConfigRelationship) CatalogReportRelationship { + r := CatalogReportRelationship{ + ConfigID: rc.ConfigID, + RelatedID: rc.RelatedID, + Relation: rc.Relation, + } + if rc.ConfigID == configID { + r.Direction = "outgoing" + } else { + r.Direction = "incoming" + } + return r +} diff --git a/catalog_report/report.go b/catalog_report/report.go new file mode 100644 index 000000000..373d107a7 --- /dev/null +++ b/catalog_report/report.go @@ -0,0 +1,485 @@ +package catalog_report + +import ( + "encoding/base64" + "fmt" + "slices" + "strings" + "time" + + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query" + "github.com/google/uuid" + "github.com/samber/lo" + + "github.com/flanksource/incident-commander/api" + "github.com/flanksource/incident-commander/db" +) + +type Options struct { + Title string + Since time.Duration + Sections api.CatalogReportSections + Recursive bool + GroupBy string // "merged" (default) or "config" + ChangeArtifacts bool +} + +func (o Options) WithDefaults() Options { + if o.Since == 0 { + o.Since = 30 * 24 * time.Hour + } + if o.Title == "" { + o.Title = "Catalog Report" + } + if o.GroupBy == "" { + o.GroupBy = "merged" + } + return o +} + +func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) (*api.CatalogReport, error) { + if len(configs) == 0 { + return nil, fmt.Errorf("no config items provided") + } + opts = opts.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + Title: opts.Title, + GeneratedAt: time.Now(), + From: sinceTime.Format(time.RFC3339), + ConfigItem: configs[0], + Sections: opts.Sections, + Recursive: opts.Recursive, + GroupBy: opts.GroupBy, + } + + report.Parents = resolveParents(ctx, &configs[0]) + + for _, config := range configs { + entry, err := buildEntry(ctx, &config, opts, sinceTime) + if err != nil { + return nil, fmt.Errorf("failed to build entry for %s: %w", config.GetName(), err) + } + report.Entries = append(report.Entries, *entry) + + report.Changes = append(report.Changes, entry.Changes...) + report.Analyses = append(report.Analyses, entry.Analyses...) + report.Access = append(report.Access, entry.Access...) + report.AccessLogs = append(report.AccessLogs, entry.AccessLogs...) + } + + if opts.Sections.ConfigJSON && configs[0].Config != nil { + report.ConfigJSON = configs[0].Config + } + + if opts.GroupBy == "config" { + report.Changes = nil + report.Analyses = nil + report.Access = nil + report.AccessLogs = nil + } + + return report, nil +} + +func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time) (*api.CatalogReportEntry, error) { + entry := &api.CatalogReportEntry{ + ConfigItem: api.NewCatalogReportConfigItem(*config), + } + + parents := resolveParents(ctx, config) + entry.Parents = lo.Map(parents, func(p models.ConfigItem, _ int) api.CatalogReportConfigItem { + return api.NewCatalogReportConfigItem(p) + }) + + tree, err := query.ConfigTree(ctx, config.ID, query.ConfigTreeOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to build config tree: %w", err) + } + + targetIDs := tree.OutgoingIDs() + configMap := make(map[uuid.UUID]models.ConfigItem) + items, err := query.GetConfigsByIDs(ctx, targetIDs) + if err != nil { + return nil, fmt.Errorf("failed to load config items: %w", err) + } + for _, ci := range items { + configMap[ci.ID] = ci + } + + configMeta := func(configID string) (string, string) { + if id, err := uuid.Parse(configID); err == nil { + if ci, ok := configMap[id]; ok { + typ := "" + if ci.Type != nil { + typ = *ci.Type + } + return ci.GetName(), typ + } + } + return "", "" + } + + catalogIDsCSV := strings.Join(lo.Map(targetIDs, func(id uuid.UUID, _ int) string { return id.String() }), ",") + + if opts.Sections.Changes { + resp, err := query.FindCatalogChanges(ctx, query.CatalogChangesSearchRequest{ + BaseCatalogSearch: query.BaseCatalogSearch{ + CatalogID: catalogIDsCSV, + FromTime: &sinceTime, + PageSize: 500, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to get changes: %w", err) + } + entry.Changes = lo.Map(resp.Changes, func(c query.ConfigChangeRow, _ int) api.CatalogReportChange { + name, typ := configMeta(c.ConfigID) + r := api.CatalogReportChange{ + ID: c.ID, + ConfigID: c.ConfigID, + ConfigName: name, + ConfigType: typ, + Permalink: api.ConfigPermalink(c.ConfigID), + ChangeType: c.ChangeType, + Severity: c.Severity, + Source: c.Source, + Summary: c.Summary, + ExternalCreatedBy: c.ExternalCreatedBy, + Count: c.Count, + } + if c.CreatedAt != nil { + r.CreatedAt = c.CreatedAt.Format(time.RFC3339) + } + if c.CreatedBy != nil { + r.CreatedBy = c.CreatedBy.String() + } + return r + }) + entry.ChangeCount = len(entry.Changes) + + if opts.ChangeArtifacts && len(entry.Changes) > 0 { + attachChangeArtifacts(ctx, entry.Changes) + } + } + + if opts.Sections.Insights { + resp, err := query.FindCatalogInsights(ctx, query.CatalogInsightsSearchRequest{ + BaseCatalogSearch: query.BaseCatalogSearch{ + CatalogID: catalogIDsCSV, + PageSize: 500, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to get insights: %w", err) + } + entry.Analyses = lo.Map(resp.Insights, func(a models.ConfigAnalysis, _ int) api.CatalogReportAnalysis { + name, typ := configMeta(a.ConfigID.String()) + return api.NewCatalogReportAnalysis(a, name, typ) + }) + entry.InsightCount = len(entry.Analyses) + } + + if opts.Sections.Access { + rbacRows, err := db.GetRBACAccessByConfigIDs(ctx, targetIDs) + if err != nil { + return nil, fmt.Errorf("failed to get access: %w", err) + } + entry.RBACResources = groupRBACByConfig(rbacRows, configMap, opts) + for _, r := range entry.RBACResources { + entry.AccessCount += len(r.Users) + } + } + + if opts.Sections.AccessLogs { + logs, err := getAccessLogs(ctx, targetIDs, sinceTime) + if err != nil { + return nil, fmt.Errorf("failed to get access logs: %w", err) + } + entry.AccessLogs = lo.Map(logs, func(l accessLogRow, _ int) api.CatalogReportAccessLog { + return newAccessLogEntry(l) + }) + } + + if opts.Sections.Relationships && tree != nil { + entry.RelationshipTree = configTreeNodeToReport(tree) + } + + return entry, nil +} + +func buildConfigGroups(report *api.CatalogReport, configMap map[uuid.UUID]models.ConfigItem) []api.CatalogReportConfigGroup { + changesByConfig := lo.GroupBy(report.Changes, func(c api.CatalogReportChange) string { return c.ConfigID }) + analysesByConfig := lo.GroupBy(report.Analyses, func(a api.CatalogReportAnalysis) string { return a.ConfigID }) + accessByConfig := lo.GroupBy(report.Access, func(a api.CatalogReportAccess) string { return a.ConfigID }) + logsByConfig := lo.GroupBy(report.AccessLogs, func(l api.CatalogReportAccessLog) string { return l.ConfigID }) + + seen := make(map[string]bool) + var groups []api.CatalogReportConfigGroup + + for _, id := range sortedConfigIDs(configMap) { + idStr := id.String() + if seen[idStr] { + continue + } + seen[idStr] = true + + changes := changesByConfig[idStr] + analyses := analysesByConfig[idStr] + access := accessByConfig[idStr] + logs := logsByConfig[idStr] + + if len(changes) == 0 && len(analyses) == 0 && len(access) == 0 && len(logs) == 0 { + continue + } + + ci := configMap[id] + groups = append(groups, api.CatalogReportConfigGroup{ + ConfigItem: api.NewCatalogReportConfigItem(ci), + Changes: changes, + Analyses: analyses, + Access: access, + AccessLogs: logs, + }) + } + return groups +} + +func sortedConfigIDs(m map[uuid.UUID]models.ConfigItem) []uuid.UUID { + ids := lo.Keys(m) + slices.SortFunc(ids, func(a, b uuid.UUID) int { + return strings.Compare(m[a].GetName(), m[b].GetName()) + }) + return ids +} + +func resolveParents(ctx context.Context, config *models.ConfigItem) []models.ConfigItem { + var parents []models.ConfigItem + current := config + for current.ParentID != nil { + loaded, err := query.GetConfigsByIDs(ctx, []uuid.UUID{*current.ParentID}) + if err != nil || len(loaded) == 0 { + break + } + parents = append(parents, loaded[0]) + current = &loaded[0] + } + for i, j := 0, len(parents)-1; i < j; i, j = i+1, j-1 { + parents[i], parents[j] = parents[j], parents[i] + } + return parents +} + +type accessLogRow struct { + ConfigID uuid.UUID `gorm:"column:config_id"` + ConfigName string `gorm:"column:config_name"` + ConfigType string `gorm:"column:config_type"` + ExternalUserID uuid.UUID `gorm:"column:external_user_id"` + UserName string `gorm:"column:user_name"` + CreatedAt time.Time `gorm:"column:created_at"` + MFA bool `gorm:"column:mfa"` + Count *int `gorm:"column:count"` + Properties map[string]any `gorm:"column:properties;serializer:json"` +} + +func (r accessLogRow) QueryLogSummary() string { + return r.ConfigType +} + +func getAccessLogs(ctx context.Context, configIDs []uuid.UUID, since time.Time) (results []accessLogRow, err error) { + timer := query.NewQueryLogger(ctx).Start("AccessLogs").Arg("configIDs", len(configIDs)) + defer timer.End(&err) + + if err = ctx.DB(). + Table("config_access_logs"). + Select(`config_access_logs.config_id, + config_items.name AS config_name, + config_items.type AS config_type, + config_access_logs.external_user_id, + external_users.name AS user_name, + config_access_logs.created_at, + config_access_logs.mfa, + config_access_logs.count, + config_access_logs.properties`). + Joins("JOIN config_items ON config_items.id = config_access_logs.config_id"). + Joins("JOIN external_users ON external_users.id = config_access_logs.external_user_id"). + Where("config_access_logs.config_id IN ?", configIDs). + Where("config_access_logs.created_at >= ?", since). + Order("config_access_logs.created_at DESC"). + Scan(&results).Error; err != nil { + return nil, err + } + timer.Results(results) + return results, nil +} + +func newAccessLogEntry(r accessLogRow) api.CatalogReportAccessLog { + var props map[string]string + if r.Properties != nil { + props = make(map[string]string, len(r.Properties)) + for k, v := range r.Properties { + props[k] = fmt.Sprintf("%v", v) + } + } + return api.CatalogReportAccessLog{ + ConfigID: r.ConfigID.String(), + Permalink: api.ConfigPermalink(r.ConfigID.String()), + UserID: r.ExternalUserID.String(), + UserName: r.UserName, + ConfigName: r.ConfigName, + ConfigType: r.ConfigType, + CreatedAt: r.CreatedAt.Format(time.RFC3339), + MFA: r.MFA, + Count: lo.FromPtr(r.Count), + Properties: props, + } +} + +func attachChangeArtifacts(ctx context.Context, changes []api.CatalogReportChange) { + changeIDs := make([]uuid.UUID, 0, len(changes)) + for _, c := range changes { + if id, err := uuid.Parse(c.ID); err == nil { + changeIDs = append(changeIDs, id) + } + } + if len(changeIDs) == 0 { + return + } + + var artifacts []models.Artifact + if err := ctx.DB().Where("config_change_id IN ?", changeIDs).Find(&artifacts).Error; err != nil { + ctx.Logger.V(2).Infof("failed to query change artifacts: %v", err) + return + } + if len(artifacts) == 0 { + return + } + + byChangeID := lo.GroupBy(artifacts, func(a models.Artifact) string { + if a.ConfigChangeID != nil { + return a.ConfigChangeID.String() + } + return "" + }) + + for i := range changes { + arts, ok := byChangeID[changes[i].ID] + if !ok { + continue + } + for _, a := range arts { + ra := api.CatalogReportArtifact{ + ID: a.ID.String(), + Filename: a.Filename, + ContentType: a.ContentType, + Size: a.Size, + } + if isEmbeddableContentType(a.ContentType) { + if data, err := a.GetContent(); err == nil && len(data) > 0 { + ra.DataURI = fmt.Sprintf("data:%s;base64,%s", a.ContentType, base64.StdEncoding.EncodeToString(data)) + } + } + changes[i].Artifacts = append(changes[i].Artifacts, ra) + } + } +} + +func isEmbeddableContentType(ct string) bool { + for _, prefix := range []string{"image/png", "image/jpeg", "image/gif", "image/webp", "image/svg"} { + if strings.HasPrefix(ct, prefix) { + return true + } + } + return false +} + +func groupRBACByConfig(rows []db.RBACAccessRow, configMap map[uuid.UUID]models.ConfigItem, opts Options) []api.RBACResource { + staleThreshold := time.Now().AddDate(0, 0, -90) + reviewThreshold := time.Now().AddDate(0, 0, -90) + + grouped := make(map[uuid.UUID]*api.RBACResource) + var order []uuid.UUID + + for _, row := range rows { + resource, ok := grouped[row.ConfigID] + if !ok { + resource = &api.RBACResource{ + ConfigID: row.ConfigID.String(), + ConfigName: row.ConfigName, + ConfigType: row.ConfigType, + } + if ci, found := configMap[row.ConfigID]; found { + resource.ConfigClass = ci.ConfigClass + resource.Path = ci.Path + if ci.Status != nil { + resource.Status = *ci.Status + } + if ci.Health != nil { + resource.Health = string(*ci.Health) + } + resource.Tags = ci.Tags + if ci.Labels != nil { + resource.Labels = *ci.Labels + } + } + grouped[row.ConfigID] = resource + order = append(order, row.ConfigID) + } + + resource.Users = append(resource.Users, api.RBACUserRole{ + UserID: row.UserID.String(), + UserName: row.UserName, + Email: row.Email, + Role: row.Role, + RoleSource: row.RoleSource(), + SourceSystem: row.UserType, + CreatedAt: row.CreatedAt, + LastSignedInAt: row.LastSignedInAt, + LastReviewedAt: row.LastReviewedAt, + IsStale: row.LastSignedInAt == nil || row.LastSignedInAt.Before(staleThreshold), + IsReviewOverdue: row.LastReviewedAt == nil || row.LastReviewedAt.Before(reviewThreshold), + }) + } + + return lo.Map(order, func(id uuid.UUID, _ int) api.RBACResource { + return *grouped[id] + }) +} + +func configTreeNodeToReport(n *query.ConfigTreeNode) *api.CatalogReportTreeNode { + result := &api.CatalogReportTreeNode{ + CatalogReportConfigItem: api.NewCatalogReportConfigItem(n.ConfigItem), + EdgeType: n.EdgeType, + Relation: n.Relation, + } + for _, c := range n.Children { + result.Children = append(result.Children, *configTreeNodeToReport(c)) + } + return result +} + +func RelatedConfigToReportItem(rc query.RelatedConfig) api.CatalogReportConfigItem { + r := api.CatalogReportConfigItem{ + ID: rc.ID.String(), + Permalink: api.ConfigPermalink(rc.ID.String()), + Name: rc.Name, + Type: rc.Type, + Tags: rc.Tags, + } + if rc.Status != nil { + r.Status = *rc.Status + } + if rc.Health != nil { + r.Health = string(*rc.Health) + } + if !rc.CreatedAt.IsZero() { + r.CreatedAt = rc.CreatedAt.Format(time.RFC3339) + } + if !rc.UpdatedAt.IsZero() { + r.UpdatedAt = rc.UpdatedAt.Format(time.RFC3339) + } + return r +} diff --git a/catalog_report/report_test.go b/catalog_report/report_test.go new file mode 100644 index 000000000..cfbaf205f --- /dev/null +++ b/catalog_report/report_test.go @@ -0,0 +1,69 @@ +package catalog_report + +import ( + "testing" + "time" + + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/flanksource/incident-commander/api" +) + +func TestCatalogReport(t *testing.T) { + RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "CatalogReport") +} + +var _ = ginkgo.Describe("Options", func() { + ginkgo.It("WithDefaults sets 30-day since", func() { + opts := Options{}.WithDefaults() + Expect(opts.Since).To(Equal(30 * 24 * time.Hour)) + }) + + ginkgo.It("WithDefaults preserves custom since", func() { + opts := Options{Since: 7 * 24 * time.Hour}.WithDefaults() + Expect(opts.Since).To(Equal(7 * 24 * time.Hour)) + }) +}) + +var _ = ginkgo.Describe("Report date range", func() { + ginkgo.It("From is set from sinceTime", func() { + opts := Options{Since: 48 * time.Hour}.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + From: sinceTime.Format(time.RFC3339), + } + + parsed, err := time.Parse(time.RFC3339, report.From) + Expect(err).ToNot(HaveOccurred()) + Expect(parsed).To(BeTemporally("~", time.Now().Add(-48*time.Hour), 2*time.Second)) + }) + + ginkgo.It("From matches sinceTime for 30-day default", func() { + opts := Options{}.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + From: sinceTime.Format(time.RFC3339), + } + + parsed, err := time.Parse(time.RFC3339, report.From) + Expect(err).ToNot(HaveOccurred()) + Expect(parsed).To(BeTemporally("~", time.Now().Add(-30*24*time.Hour), 2*time.Second)) + }) + + ginkgo.It("query FromTime matches report From", func() { + opts := Options{Since: 7 * 24 * time.Hour}.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + From: sinceTime.Format(time.RFC3339), + } + + reportFrom, err := time.Parse(time.RFC3339, report.From) + Expect(err).ToNot(HaveOccurred()) + Expect(reportFrom).To(BeTemporally("~", sinceTime, time.Second)) + }) +}) diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index f868550dd..8403ab2b8 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -1,17 +1,20 @@ import React from 'react'; -import { Page, PageBreak, Section } from '@flanksource/facet'; +import { Page, PageBreak, Section, ListTable } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; -import type { CatalogReportData } from './catalog-report-types.ts'; +import type { CatalogReportData, CatalogReportConfigGroup, CatalogReportEntry } from './catalog-report-types.ts'; import ConfigChangesSection from './components/ConfigChangesSection.tsx'; import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; import ConfigRelationshipGraph from './components/ConfigRelationshipGraph.tsx'; +import ConfigTreeSection from './components/ConfigTreeSection.tsx'; import CatalogAccessSection from './components/CatalogAccessSection.tsx'; import CatalogAccessLogsSection from './components/CatalogAccessLogsSection.tsx'; +import RBACMatrixSection, { MatrixLegend } from './components/RBACMatrixSection.tsx'; +import ArtifactAppendix from './components/ArtifactAppendix.tsx'; import { formatDate, formatDateTime } from './components/utils.ts'; function PageHeader({ title }: { title: string }) { return ( -
+
{title} Catalog Report
@@ -21,22 +24,22 @@ function PageHeader({ title }: { title: string }) { function PageFooter() { const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); return ( -
+
Generated {now}
); } function CoverPage({ data }: { data: CatalogReportData }) { - const ci = data.configItem; - const tags = { ...ci.tags, ...ci.labels }; + const ci = data.configItem || {}; + const tags = { ...(ci.tags || {}), ...(ci.labels || {}) }; return (
-
{data.title || 'Catalog Report'}
+
{data.title || 'Catalog Report'}
- {data.parents.length > 0 && ( -
+ {(data.parents || []).length > 0 && ( +
{data.parents.map((p, i) => ( {i > 0 && ' › '} @@ -48,15 +51,15 @@ function CoverPage({ data }: { data: CatalogReportData }) {
{ci.type && } - {ci.name} + {ci.name}
- {ci.type &&
{ci.type}
} + {ci.type &&
{ci.type}
} {Object.keys(tags).length > 0 && (
{Object.entries(tags).map(([k, v]) => ( - + {k} {v || '-'} @@ -64,22 +67,35 @@ function CoverPage({ data }: { data: CatalogReportData }) {
)} -
- {ci.health && Health: {ci.health}} - {ci.status && Status: {ci.status}} +
+ {!isUnknown(ci.health) && Health: {ci.health}} + {!isUnknown(ci.status) && Status: {ci.status}} {ci.created_at && Created: {formatDate(ci.created_at)}}
-
+ {(data.from || data.to) && ( +
+ Period: {data.from ? formatDate(data.from) : '...'} – {data.to ? formatDate(data.to) : 'now'} +
+ )} + +
Generated {data.generatedAt ? formatDateTime(data.generatedAt) : new Date().toLocaleDateString()}
-
- {data.sections.changes && {data.changes.length} changes} - {data.sections.insights && {data.analyses.length} insights} - {data.sections.relationships && {data.relatedConfigs.length} relationships} - {data.sections.access && {data.access.length} access entries} - {data.sections.accessLogs && {data.accessLogs.length} access logs} + {data.recursive && ( +
+ Including all descendant config items + {data.groupBy === 'config' && ` · Grouped by config (${(data.configGroups || []).length} items)`} +
+ )} + +
+ {data.sections?.changes && {(data.changes || []).length} changes} + {data.sections?.insights && {(data.analyses || []).length} insights} + {data.sections?.relationships && {(data.relatedConfigs || []).length} relationships} + {data.sections?.access && {(data.access || []).length} access entries} + {data.sections?.accessLogs && {(data.accessLogs || []).length} access logs}
); @@ -89,17 +105,64 @@ function ConfigJSONSection({ json }: { json: string }) { let formatted = json; try { formatted = JSON.stringify(JSON.parse(json), null, 2); - } catch {} + } catch { } return (
-
+      
         {formatted}
       
); } +function ConfigGroupHeader({ group }: { group: CatalogReportConfigGroup }) { + const ci = group.configItem; + return ( +
+ {ci.type && } + {ci.name} + {ci.type && {ci.type}} + {ci.permalink && ( + {ci.permalink} + )} +
+ ); +} + +function isUnknown(v?: string): boolean { + return !v || v.toLowerCase() === 'unknown'; +} + +function entryToRow(entry: CatalogReportEntry): Record { + const ci = entry.configItem || {}; + const row: Record = { + name: ci.name, + type: ci.type, + }; + if (!isUnknown(ci.health)) row.health = ci.health; + if (!isUnknown(ci.status)) row.status = ci.status; + if (entry.changeCount > 0) row.changes = `${entry.changeCount} changes`; + if (entry.insightCount > 0) row.insights = `${entry.insightCount} insights`; + if (entry.accessCount > 0) row.access = `${entry.accessCount} access`; + return row; +} + +function EntryDetail({ entry }: { entry: CatalogReportEntry }) { + const hasTree = entry.relationshipTree && (entry.relationshipTree.children || []).length > 0; + const hasRbac = (entry.rbacResources || []).length > 0; + if (!hasTree && !hasRbac) return null; + + return ( +
+ {hasTree && } + {hasRbac && entry.rbacResources!.map((resource, idx) => ( + + ))} +
+ ); +} + interface CatalogReportProps { data: CatalogReportData; } @@ -136,7 +199,65 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { - {data.sections.changes && data.changes.length > 0 && ( + {(data.entries || []).length > 0 && ( + <> + + +
+ type ? : null} + primaryTags={['health', 'status']} + secondaryTags={['changes', 'insights', 'access']} + size="sm" + density="compact" + /> + {data.entries!.map((entry, idx) => ( + + ))} +
+
+ + )} + + {data.groupBy === 'config' && (data.entries || []).map((entry, idx) => ( + + {(entry.changes || []).length > 0 && ( + <> + + + + + + + )} + {(entry.analyses || []).length > 0 && ( + <> + + + + + + + )} + {(entry.rbacResources || []).length > 0 && ( + <> + + + + {entry.rbacResources!.map((resource, rIdx) => ( + + ))} + + + )} + + ))} + + {data.groupBy !== 'config' && data.sections?.changes && (data.changes || []).length > 0 && ( <> @@ -145,7 +266,7 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { )} - {data.sections.insights && data.analyses.length > 0 && ( + {data.groupBy !== 'config' && data.sections?.insights && (data.analyses || []).length > 0 && ( <> @@ -154,7 +275,16 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { )} - {data.sections.relationships && data.relatedConfigs.length > 0 && ( + {data.sections?.relationships && data.relationshipTree && ( + <> + + + + + + )} + + {data.sections?.relationships && !data.relationshipTree && (data.relatedConfigs || []).length > 0 && ( <> @@ -167,7 +297,7 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { )} - {data.sections.access && data.access.length > 0 && ( + {data.groupBy !== 'config' && data.sections?.access && (data.access || []).length > 0 && ( <> @@ -176,7 +306,7 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { )} - {data.sections.accessLogs && data.accessLogs.length > 0 && ( + {data.groupBy !== 'config' && data.sections?.accessLogs && (data.accessLogs || []).length > 0 && ( <> @@ -185,7 +315,27 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { )} - {data.sections.configJSON && data.configJSON && ( + {data.groupBy === 'config' && (data.configGroups || []).map((group, idx) => ( + + + + {(group.changes || []).length > 0 && ( + + )} + {(group.analyses || []).length > 0 && ( + + )} + {(group.access || []).length > 0 && ( + + )} + {(group.accessLogs || []).length > 0 && ( + + )} + + + ))} + + {data.sections?.configJSON && data.configJSON && ( <> @@ -193,6 +343,20 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { )} + + {(() => { + const allChanges = (data.entries || []).flatMap((e) => e.changes || []); + const withArtifacts = allChanges.filter((c) => (c.artifacts || []).length > 0); + if (withArtifacts.length === 0) return null; + return ( + <> + + + + + + ); + })()} ); } diff --git a/report/catalog-report-types.ts b/report/catalog-report-types.ts index b3ce5828f..41a245337 100644 --- a/report/catalog-report-types.ts +++ b/report/catalog-report-types.ts @@ -1,4 +1,5 @@ import type { ConfigChange, ConfigAnalysis, ConfigRelationship, ConfigItem } from './config-types.ts'; +import type { RBACResource } from './rbac-types.ts'; export interface CatalogReportSections { changes: boolean; @@ -10,6 +11,10 @@ export interface CatalogReportSections { } export interface CatalogReportAccess { + configId?: string; + configName?: string; + configType?: string; + permalink?: string; userId: string; userName: string; email: string; @@ -21,6 +26,8 @@ export interface CatalogReportAccess { } export interface CatalogReportAccessLog { + configId?: string; + permalink?: string; userId: string; userName: string; configName: string; @@ -31,9 +38,28 @@ export interface CatalogReportAccessLog { properties?: Record; } +export interface CatalogReportTreeNode extends ConfigItem { + edgeType?: 'parent' | 'child' | 'related' | 'target'; + relation?: string; + permalink?: string; + children?: CatalogReportTreeNode[]; +} + +export interface CatalogReportConfigGroup { + configItem: ConfigItem; + changes: ConfigChange[]; + analyses: ConfigAnalysis[]; + access: CatalogReportAccess[]; + accessLogs: CatalogReportAccessLog[]; +} + export interface CatalogReportData { title: string; generatedAt: string; + from?: string; + to?: string; + recursive?: boolean; + groupBy?: string; configItem: ConfigItem & { config?: string; name: string; @@ -58,4 +84,21 @@ export interface CatalogReportData { access: CatalogReportAccess[]; accessLogs: CatalogReportAccessLog[]; configJSON?: string; + configGroups?: CatalogReportConfigGroup[]; + relationshipTree?: CatalogReportTreeNode; + entries?: CatalogReportEntry[]; +} + +export interface CatalogReportEntry { + configItem: ConfigItem & { permalink?: string }; + parents?: Array; + relationshipTree?: CatalogReportTreeNode; + changeCount: number; + insightCount: number; + accessCount: number; + rbacResources?: RBACResource[]; + changes: ConfigChange[]; + analyses: ConfigAnalysis[]; + access: CatalogReportAccess[]; + accessLogs: CatalogReportAccessLog[]; } From b3a4a702dc4c01e7787b6238078d0d0fb5c48c3e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 6 Apr 2026 21:16:57 +0300 Subject: [PATCH 27/48] fix(report): use actual dates instead of 'now' in cover page date range --- report/CatalogReport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index 8403ab2b8..4abb0c757 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -75,7 +75,7 @@ function CoverPage({ data }: { data: CatalogReportData }) { {(data.from || data.to) && (
- Period: {data.from ? formatDate(data.from) : '...'} – {data.to ? formatDate(data.to) : 'now'} + Period: {formatDate(data.from || data.generatedAt)} – {formatDate(data.to || data.generatedAt)}
)} From 54ff4504fb4e27cd422e9315196b3addcba490d9 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Mon, 6 Apr 2026 21:58:57 +0300 Subject: [PATCH 28/48] refactor(report): extract shared CoverPage and CatalogList components - Create CoverPage component with unified props for title, subtitle, icon, breadcrumbs, subjects, tags, stats, dateRange, and children slot - Create CatalogList component extracting ListTable + EntryDetail from CatalogReport - Update all four report types (Application, ViewReport, CatalogReport, RBAC) to use shared CoverPage --- report/Application.tsx | 43 ++------ report/CatalogReport.tsx | 137 +++++------------------- report/ViewReport.tsx | 54 +++------- report/components/CatalogList.tsx | 64 +++++++++++ report/components/CoverPage.tsx | 140 +++++++++++++++++++++++++ report/components/RBACCoverContent.tsx | 95 +++-------------- 6 files changed, 271 insertions(+), 262 deletions(-) create mode 100644 report/components/CatalogList.tsx create mode 100644 report/components/CoverPage.tsx diff --git a/report/Application.tsx b/report/Application.tsx index 8dee6c7ab..a0f1b9137 100644 --- a/report/Application.tsx +++ b/report/Application.tsx @@ -8,10 +8,11 @@ import BackupsSection from './components/BackupsSection.tsx'; import FindingsSection from './components/FindingsSection.tsx'; import LocationsSection from './components/LocationsSection.tsx'; import DynamicSection from './components/DynamicSection.tsx'; +import CoverPage from './components/CoverPage.tsx'; function PageHeader({ app }: { app: Application }) { return ( -
+
{app.name} Application Report
@@ -23,42 +24,20 @@ function PageFooter() { year: 'numeric', month: 'long', day: 'numeric' }); return ( -
+
Generated {date}
); } -function CoverContent({ app }: { app: Application }) { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric' - }); +function AppCoverPage({ app }: { app: Application }) { return ( -
-
-
- Application Report -
-

- {app.name} -

-
- {app.type} · {app.namespace} -
- {app.description && ( -

- {app.description} -

- )} -
-
-
- Generated on {date} -
-
+ ); } @@ -82,7 +61,7 @@ export default function ApplicationReport({ data }: ApplicationReportProps) { <> {/* Cover page — no header/footer */} - + diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index 4abb0c757..1892d94ef 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -1,16 +1,17 @@ import React from 'react'; -import { Page, PageBreak, Section, ListTable } from '@flanksource/facet'; +import { Page, PageBreak, Section } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; -import type { CatalogReportData, CatalogReportConfigGroup, CatalogReportEntry } from './catalog-report-types.ts'; +import type { CatalogReportData, CatalogReportConfigGroup } from './catalog-report-types.ts'; import ConfigChangesSection from './components/ConfigChangesSection.tsx'; import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; import ConfigRelationshipGraph from './components/ConfigRelationshipGraph.tsx'; import ConfigTreeSection from './components/ConfigTreeSection.tsx'; import CatalogAccessSection from './components/CatalogAccessSection.tsx'; import CatalogAccessLogsSection from './components/CatalogAccessLogsSection.tsx'; -import RBACMatrixSection, { MatrixLegend } from './components/RBACMatrixSection.tsx'; +import RBACMatrixSection from './components/RBACMatrixSection.tsx'; import ArtifactAppendix from './components/ArtifactAppendix.tsx'; -import { formatDate, formatDateTime } from './components/utils.ts'; +import CoverPage from './components/CoverPage.tsx'; +import CatalogList from './components/CatalogList.tsx'; function PageHeader({ title }: { title: string }) { return ( @@ -30,74 +31,32 @@ function PageFooter() { ); } -function CoverPage({ data }: { data: CatalogReportData }) { +function CatalogCoverPage({ data }: { data: CatalogReportData }) { const ci = data.configItem || {}; - const tags = { ...(ci.tags || {}), ...(ci.labels || {}) }; + const stats: Array<{ label: string; value: number }> = []; + if (data.sections?.changes) stats.push({ label: 'changes', value: (data.changes || []).length }); + if (data.sections?.insights) stats.push({ label: 'insights', value: (data.analyses || []).length }); + if (data.sections?.relationships) stats.push({ label: 'relationships', value: (data.relatedConfigs || []).length }); + if (data.sections?.access) stats.push({ label: 'access entries', value: (data.access || []).length }); + if (data.sections?.accessLogs) stats.push({ label: 'access logs', value: (data.accessLogs || []).length }); return ( -
-
{data.title || 'Catalog Report'}
- - {(data.parents || []).length > 0 && ( -
- {data.parents.map((p, i) => ( - - {i > 0 && ' › '} - {p.name} - - ))} -
- )} - -
- {ci.type && } - {ci.name} -
- - {ci.type &&
{ci.type}
} - - {Object.keys(tags).length > 0 && ( -
- {Object.entries(tags).map(([k, v]) => ( - - {k} - {v || '-'} - - ))} -
- )} - -
- {!isUnknown(ci.health) && Health: {ci.health}} - {!isUnknown(ci.status) && Status: {ci.status}} - {ci.created_at && Created: {formatDate(ci.created_at)}} -
- - {(data.from || data.to) && ( -
- Period: {formatDate(data.from || data.generatedAt)} – {formatDate(data.to || data.generatedAt)} -
- )} - -
- Generated {data.generatedAt ? formatDateTime(data.generatedAt) : new Date().toLocaleDateString()} -
- + {data.recursive && (
Including all descendant config items {data.groupBy === 'config' && ` · Grouped by config (${(data.configGroups || []).length} items)`}
)} - -
- {data.sections?.changes && {(data.changes || []).length} changes} - {data.sections?.insights && {(data.analyses || []).length} insights} - {data.sections?.relationships && {(data.relatedConfigs || []).length} relationships} - {data.sections?.access && {(data.access || []).length} access entries} - {data.sections?.accessLogs && {(data.accessLogs || []).length} access logs} -
-
+
); } @@ -130,39 +89,6 @@ function ConfigGroupHeader({ group }: { group: CatalogReportConfigGroup }) { ); } -function isUnknown(v?: string): boolean { - return !v || v.toLowerCase() === 'unknown'; -} - -function entryToRow(entry: CatalogReportEntry): Record { - const ci = entry.configItem || {}; - const row: Record = { - name: ci.name, - type: ci.type, - }; - if (!isUnknown(ci.health)) row.health = ci.health; - if (!isUnknown(ci.status)) row.status = ci.status; - if (entry.changeCount > 0) row.changes = `${entry.changeCount} changes`; - if (entry.insightCount > 0) row.insights = `${entry.insightCount} insights`; - if (entry.accessCount > 0) row.access = `${entry.accessCount} access`; - return row; -} - -function EntryDetail({ entry }: { entry: CatalogReportEntry }) { - const hasTree = entry.relationshipTree && (entry.relationshipTree.children || []).length > 0; - const hasRbac = (entry.rbacResources || []).length > 0; - if (!hasTree && !hasRbac) return null; - - return ( -
- {hasTree && } - {hasRbac && entry.rbacResources!.map((resource, idx) => ( - - ))} -
- ); -} - interface CatalogReportProps { data: CatalogReportData; } @@ -196,29 +122,14 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { return ( <> - + {(data.entries || []).length > 0 && ( <> -
- type ? : null} - primaryTags={['health', 'status']} - secondaryTags={['changes', 'insights', 'access']} - size="sm" - density="compact" - /> - {data.entries!.map((entry, idx) => ( - - ))} -
+
)} diff --git a/report/ViewReport.tsx b/report/ViewReport.tsx index e681ecda8..5e851425d 100644 --- a/report/ViewReport.tsx +++ b/report/ViewReport.tsx @@ -3,10 +3,11 @@ import { Page, PageBreak } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ViewReportData, MultiViewReportData } from './view-types.ts'; import ViewResultSection from './components/ViewResultSection.tsx'; +import CoverPage from './components/CoverPage.tsx'; function PageHeader({ title, icon }: { title: string; icon?: string }) { return ( -
+
{icon && } {title} @@ -21,46 +22,25 @@ function PageFooter() { year: 'numeric', month: 'long', day: 'numeric', }); return ( -
+
Generated {date}
); } -function CoverContent({ data }: { data: ViewReportData }) { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); +function ViewCoverPage({ data }: { data: ViewReportData }) { + const variableTags = (data.variables || []).reduce((acc, v) => { + acc[v.label || v.key] = v.default || '-'; + return acc; + }, {} as Record); + return ( -
-
- {data.icon && ( -
- -
- )} -

- {data.title || data.name} -

- {data.namespace && ( -
- {data.namespace}/{data.name} -
- )} -
- {data.variables && data.variables.length > 0 && ( -
- {data.variables.map((v) => ( - - {v.label || v.key}: - {v.default || '-'} - - ))} -
- )} -
-
Generated on {date}
-
+ 0 ? variableTags : undefined} + /> ); } @@ -90,14 +70,14 @@ export default function ViewReportPage({ data }: ViewReportProps) { return ( <> - + {viewsList.map((view, idx) => ( -
+
diff --git a/report/components/CatalogList.tsx b/report/components/CatalogList.tsx new file mode 100644 index 000000000..37ec3d9ad --- /dev/null +++ b/report/components/CatalogList.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Section, ListTable } from '@flanksource/facet'; +import { Icon } from '@flanksource/icons/icon'; +import type { CatalogReportEntry } from '../catalog-report-types.ts'; +import ConfigTreeSection from './ConfigTreeSection.tsx'; +import RBACMatrixSection from './RBACMatrixSection.tsx'; + +function isUnknown(v?: string): boolean { + return !v || v.toLowerCase() === 'unknown'; +} + +function entryToRow(entry: CatalogReportEntry): Record { + const ci = entry.configItem; + const row: Record = { + name: ci.name, + type: ci.type, + }; + if (!isUnknown(ci.health)) row.health = ci.health; + if (!isUnknown(ci.status)) row.status = ci.status; + if (entry.changeCount > 0) row.changes = `${entry.changeCount} changes`; + if (entry.insightCount > 0) row.insights = `${entry.insightCount} insights`; + if (entry.accessCount > 0) row.access = `${entry.accessCount} access`; + return row; +} + +function EntryDetail({ entry }: { entry: CatalogReportEntry }) { + const hasTree = entry.relationshipTree && (entry.relationshipTree.children || []).length > 0; + const hasRbac = (entry.rbacResources || []).length > 0; + if (!hasTree && !hasRbac) return null; + + return ( +
+ {hasTree && } + {hasRbac && entry.rbacResources!.map((resource, idx) => ( + + ))} +
+ ); +} + +interface CatalogListProps { + entries: CatalogReportEntry[]; +} + +export default function CatalogList({ entries }: CatalogListProps) { + return ( +
+ type ? : null} + primaryTags={['health', 'status']} + secondaryTags={['changes', 'insights', 'access']} + size="sm" + density="compact" + /> + {entries.map((entry, idx) => ( + + ))} +
+ ); +} diff --git a/report/components/CoverPage.tsx b/report/components/CoverPage.tsx new file mode 100644 index 000000000..aa0fab229 --- /dev/null +++ b/report/components/CoverPage.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; +import { formatDate, formatDateTime } from './utils.ts'; + +interface CoverPageSubject { + name?: string; + type?: string; + status?: string; + health?: string; + description?: string; + tags?: Record; + labels?: Record; +} + +interface CoverPageProps { + title: string; + subtitle?: string; + icon?: string; + query?: string; + breadcrumbs?: Array<{ id: string; name?: string; type?: string }>; + subjects?: CoverPageSubject[]; + tags?: Record; + stats?: Array<{ label: string; value: string | number }>; + dateRange?: { from?: string; to?: string }; + generatedAt?: string; + children?: React.ReactNode; +} + +function SubjectBadge({ subject }: { subject: CoverPageSubject }) { + return ( +
+ {subject.type && } + {subject.name} + {subject.type} + {subject.status && ( + + {subject.status} + + )} +
+ ); +} + +function TagBadges({ tags }: { tags: Record }) { + if (Object.keys(tags).length === 0) return null; + return ( +
+ {Object.entries(tags).map(([k, v]) => ( + + {k} + {v || '-'} + + ))} +
+ ); +} + +export default function CoverPage({ title, subtitle, icon, query, breadcrumbs, subjects, tags, stats, dateRange, generatedAt, children }: CoverPageProps) { + const allTags = tags || {}; + if (!tags && subjects?.length === 1) { + Object.assign(allTags, subjects[0].tags || {}, subjects[0].labels || {}); + } + + return ( +
+
+ {subtitle && ( +
+ {subtitle} +
+ )} + + {icon && ( +
+ +
+ )} + +

{title}

+ + {query && ( +
+ {query} +
+ )} +
+ + {breadcrumbs && breadcrumbs.length > 0 && ( +
+ {breadcrumbs.map((p, i) => ( + + {i > 0 && } + + {p.type && } + {p.name} + + + ))} +
+ )} + + {subjects && subjects.length > 0 && ( +
+ {subjects.map((s, i) => ( + + ))} + {subjects.length === 1 && subjects[0].description && ( +
+ {subjects[0].description} +
+ )} +
+ )} + + {Object.keys(allTags).length > 0 && } + +
+ + {stats && stats.length > 0 && ( +
+ {stats.map((s) => ( + {s.value} {s.label} + ))} +
+ )} + + {dateRange && (dateRange.from || dateRange.to) && ( +
+ Period: {formatDate(dateRange.from || generatedAt || new Date().toISOString())} – {formatDate(dateRange.to || generatedAt || new Date().toISOString())} +
+ )} + +
+ Generated {generatedAt ? formatDateTime(generatedAt) : new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} +
+ + {children} +
+ ); +} diff --git a/report/components/RBACCoverContent.tsx b/report/components/RBACCoverContent.tsx index d7ee4389d..b933d5b97 100644 --- a/report/components/RBACCoverContent.tsx +++ b/report/components/RBACCoverContent.tsx @@ -1,36 +1,6 @@ import React from 'react'; -import { Icon } from '@flanksource/icons/icon'; -import type { RBACReport, ConfigItem } from '../rbac-types.ts'; - -function ConfigBadge({ config }: { config: ConfigItem }) { - return ( -
- {config.type && } - {config.name} - {config.type} - {config.status && ( - - {config.status} - - )} -
- ); -} - -function TagBadges({ tags, labels }: { tags?: Record; labels?: Record }) { - const all = { ...tags, ...labels }; - if (Object.keys(all).length === 0) return null; - return ( -
- {Object.entries(all).map(([k, v]) => ( - - {k} - {v || '-'} - - ))} -
- ); -} +import type { RBACReport } from '../rbac-types.ts'; +import CoverPage from './CoverPage.tsx'; interface Props { report: RBACReport; @@ -38,56 +8,21 @@ interface Props { } export default function RBACCoverContent({ report, subtitle }: Props) { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); const { subject, parents } = report; + const subjects = subject ? [subject] : undefined; return ( -
-
-
- {subtitle} -
-

- {report.title} -

- {report.query && ( -
- {report.query} -
- )} -
- - {subject && ( -
- {parents && parents.length > 0 && ( -
- {parents.map((p, i) => ( - - {i > 0 && } - - {p.type && } - {p.name} - - - ))} -
- )} - - {subject.description && ( -
- {subject.description} -
- )} - -
- )} - -
- -
- {report.summary.totalResources} resources · {report.summary.totalUsers} users · {report.summary.staleAccessCount} stale -
-
Generated {now}
-
+ ); } From e403488025a54f4e6ce309d5ee1e1efb00285412 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 7 Apr 2026 06:44:55 +0300 Subject: [PATCH 29/48] feat(api): add catalog and tree report commands with export support Adds `catalog report` and `catalog tree` commands with configurable export formats (JSON, HTML, PDF), time-range filtering, and recursive config item support. Enhances RBAC and catalog reports with additional metadata fields (config class, path, subject, parents). Refactors report rendering to use Document/Header/Footer model for cleaner page layout. --- api/catalog_report.go | 1 + api/rbac_report.go | 22 +- catalog_report/export.go | 92 ++++++++ catalog_report/render_facet.go | 35 +++ catalog_report/report.go | 1 + cmd/catalog.go | 2 +- cmd/catalog_get.go | 94 ++++++-- cmd/catalog_report.go | 144 ++++++++++++ cmd/catalog_tree.go | 154 +++++++++++++ rbac_report/export.go | 10 +- rbac_report/render_facet.go | 19 +- rbac_report/report.go | 46 +++- report/Application.tsx | 119 ++-------- report/CatalogReport.tsx | 216 ++++-------------- report/MatrixDemo.tsx | 16 +- report/RBACByUserReport.tsx | 75 ++---- report/RBACMatrixReport.tsx | 87 ++----- report/ViewReport.tsx | 65 ++---- report/catalog-report-types.ts | 1 + report/components/ArtifactAppendix.tsx | 69 ++++++ .../components/CatalogAccessLogsSection.tsx | 22 +- report/components/CatalogAccessSection.tsx | 67 +++--- report/components/CatalogList.tsx | 3 +- report/components/ConfigChangesSection.tsx | 89 ++++++-- report/components/ConfigInsightsSection.tsx | 29 ++- report/components/ConfigRelationshipGraph.tsx | 21 +- report/components/ConfigTreeSection.tsx | 51 +++++ report/components/PageFooter.tsx | 24 ++ report/components/PageHeader.tsx | 15 ++ report/components/RBACChangelogSection.tsx | 4 +- report/components/RBACMatrixSection.tsx | 4 +- report/components/RBACSummarySection.tsx | 4 +- report/components/RBACUserSection.tsx | 4 +- report/components/rbac-visual.tsx | 15 +- report/components/utils.ts | 60 ++++- report/config-types.ts | 16 ++ report/embed.go | 30 ++- report/facet.go | 7 +- report/view-types.ts | 3 + 39 files changed, 1134 insertions(+), 602 deletions(-) create mode 100644 catalog_report/export.go create mode 100644 catalog_report/render_facet.go create mode 100644 cmd/catalog_report.go create mode 100644 cmd/catalog_tree.go create mode 100644 report/components/ArtifactAppendix.tsx create mode 100644 report/components/ConfigTreeSection.tsx create mode 100644 report/components/PageFooter.tsx create mode 100644 report/components/PageHeader.tsx diff --git a/api/catalog_report.go b/api/catalog_report.go index 1ab4f4ebd..54a1c88ce 100644 --- a/api/catalog_report.go +++ b/api/catalog_report.go @@ -17,6 +17,7 @@ func ConfigPermalink(configID string) string { type CatalogReport struct { Title string `json:"title"` GeneratedAt time.Time `json:"generatedAt"` + PublicURL string `json:"publicURL,omitempty"` From string `json:"from,omitempty"` To string `json:"to,omitempty"` Sections CatalogReportSections `json:"sections"` diff --git a/api/rbac_report.go b/api/rbac_report.go index 24aa3555f..72f870975 100644 --- a/api/rbac_report.go +++ b/api/rbac_report.go @@ -3,23 +3,29 @@ package api import ( "time" + "github.com/flanksource/duty/models" "github.com/flanksource/duty/types" ) type RBACReport struct { - Title string `json:"title"` - Query string `json:"query,omitempty"` - GeneratedAt time.Time `json:"generatedAt"` - Resources []RBACResource `json:"resources"` - Changelog []RBACChangeEntry `json:"changelog"` - Summary RBACSummary `json:"summary"` - Users []RBACUserReport `json:"users,omitempty"` + Title string `json:"title"` + Query string `json:"query,omitempty"` + GeneratedAt time.Time `json:"generatedAt"` + Subject *models.ConfigItem `json:"subject,omitempty"` + Parents []models.ConfigItem `json:"parents,omitempty"` + Resources []RBACResource `json:"resources"` + Changelog []RBACChangeEntry `json:"changelog"` + Summary RBACSummary `json:"summary"` + Users []RBACUserReport `json:"users,omitempty"` } type RBACResource struct { ConfigID string `json:"configId"` ConfigName string `json:"configName"` ConfigType string `json:"configType"` + ConfigClass string `json:"configClass,omitempty"` + ParentID string `json:"parentId,omitempty"` + Path string `json:"path,omitempty"` Status string `json:"status,omitempty"` Health string `json:"health,omitempty"` Description string `json:"description,omitempty"` @@ -71,6 +77,8 @@ type RBACUserResource struct { ConfigID string `json:"configId"` ConfigName string `json:"configName"` ConfigType string `json:"configType"` + ConfigClass string `json:"configClass,omitempty"` + Path string `json:"path,omitempty"` Role string `json:"role"` RoleSource string `json:"roleSource"` CreatedAt time.Time `json:"createdAt"` diff --git a/catalog_report/export.go b/catalog_report/export.go new file mode 100644 index 000000000..3c6c05b89 --- /dev/null +++ b/catalog_report/export.go @@ -0,0 +1,92 @@ +package catalog_report + +import ( + "encoding/json" + + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/models" + + "github.com/flanksource/incident-commander/api" +) + +func Export(ctx context.Context, configs []models.ConfigItem, opts Options, format string) ([]byte, error) { + report, err := BuildReport(ctx, configs, opts) + if err != nil { + return nil, err + } + + ctx.Logger.V(3).Infof("Report built: %d entries, %d changes, %d analyses", + len(report.Entries), len(report.Changes), len(report.Analyses)) + + switch format { + case "html", "facet-html": + return RenderFacetHTML(ctx, report) + case "pdf", "facet-pdf": + return RenderFacetPDF(ctx, report) + default: + return json.MarshalIndent(report, "", " ") + } +} + +func initSlices(r *api.CatalogReport) api.CatalogReport { + out := *r + if out.Entries == nil { + out.Entries = []api.CatalogReportEntry{} + } + for i := range out.Entries { + if out.Entries[i].Parents == nil { + out.Entries[i].Parents = []api.CatalogReportConfigItem{} + } + if out.Entries[i].Changes == nil { + out.Entries[i].Changes = []api.CatalogReportChange{} + } + if out.Entries[i].Analyses == nil { + out.Entries[i].Analyses = []api.CatalogReportAnalysis{} + } + if out.Entries[i].Access == nil { + out.Entries[i].Access = []api.CatalogReportAccess{} + } + if out.Entries[i].AccessLogs == nil { + out.Entries[i].AccessLogs = []api.CatalogReportAccessLog{} + } + } + if out.Parents == nil { + out.Parents = []models.ConfigItem{} + } + if out.Changes == nil { + out.Changes = []api.CatalogReportChange{} + } + if out.Analyses == nil { + out.Analyses = []api.CatalogReportAnalysis{} + } + if out.Relationships == nil { + out.Relationships = []api.CatalogReportRelationship{} + } + if out.RelatedConfigs == nil { + out.RelatedConfigs = []api.CatalogReportConfigItem{} + } + if out.Access == nil { + out.Access = []api.CatalogReportAccess{} + } + if out.AccessLogs == nil { + out.AccessLogs = []api.CatalogReportAccessLog{} + } + if out.ConfigGroups == nil { + out.ConfigGroups = []api.CatalogReportConfigGroup{} + } + for i := range out.ConfigGroups { + if out.ConfigGroups[i].Changes == nil { + out.ConfigGroups[i].Changes = []api.CatalogReportChange{} + } + if out.ConfigGroups[i].Analyses == nil { + out.ConfigGroups[i].Analyses = []api.CatalogReportAnalysis{} + } + if out.ConfigGroups[i].Access == nil { + out.ConfigGroups[i].Access = []api.CatalogReportAccess{} + } + if out.ConfigGroups[i].AccessLogs == nil { + out.ConfigGroups[i].AccessLogs = []api.CatalogReportAccessLog{} + } + } + return out +} diff --git a/catalog_report/render_facet.go b/catalog_report/render_facet.go new file mode 100644 index 000000000..b4dcc4d6d --- /dev/null +++ b/catalog_report/render_facet.go @@ -0,0 +1,35 @@ +package catalog_report + +import ( + "fmt" + + "github.com/flanksource/duty/context" + + "github.com/flanksource/incident-commander/api" + "github.com/flanksource/incident-commander/report" +) + +func RenderFacetHTML(ctx context.Context, r *api.CatalogReport) ([]byte, error) { + return renderWithFacet(ctx, r, "html") +} + +func RenderFacetPDF(ctx context.Context, r *api.CatalogReport) ([]byte, error) { + return renderWithFacet(ctx, r, "pdf") +} + +func renderWithFacet(ctx context.Context, r *api.CatalogReport, format string) ([]byte, error) { + if r == nil { + return nil, fmt.Errorf("catalog report must not be nil") + } + + ctx.Logger.V(3).Infof("Rendering catalog facet-%s", format) + + data := initSlices(r) + result, err := report.RenderCLI(data, format, "CatalogReport.tsx") + if err != nil { + return nil, err + } + + ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result)/1024, format) + return result, nil +} diff --git a/catalog_report/report.go b/catalog_report/report.go index 373d107a7..57dac27c1 100644 --- a/catalog_report/report.go +++ b/catalog_report/report.go @@ -49,6 +49,7 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) report := &api.CatalogReport{ Title: opts.Title, GeneratedAt: time.Now(), + PublicURL: api.FrontendURL, From: sinceTime.Format(time.RFC3339), ConfigItem: configs[0], Sections: opts.Sections, diff --git a/cmd/catalog.go b/cmd/catalog.go index 6460f1756..a7baaf5e7 100644 --- a/cmd/catalog.go +++ b/cmd/catalog.go @@ -128,7 +128,7 @@ func init() { Query.Flags().DurationVarP(&catalogWaitFor, "wait", "w", 60*time.Second, "Wait for this long for resources to be discovered") clicky.BindAllFlags(Query.PersistentFlags(), "format") - Get.Flags().DurationVar(&catalogGetSince, "since", 7*24*time.Hour, "Show changes and playbook runs since this duration ago") + Get.Flags().StringVar(&catalogGetSince, "since", "7d", "Time range for changes (supports d/w/y e.g. 7d, 2w, 30d)") clicky.BindAllFlags(Get.PersistentFlags(), "format") Catalog.AddCommand(Query) diff --git a/cmd/catalog_get.go b/cmd/catalog_get.go index b69de6700..4bdcebd1d 100644 --- a/cmd/catalog_get.go +++ b/cmd/catalog_get.go @@ -8,6 +8,7 @@ import ( "github.com/flanksource/clicky" "github.com/flanksource/clicky/api" + "github.com/flanksource/commons/duration" "github.com/flanksource/commons/logger" "github.com/flanksource/commons/properties" "github.com/flanksource/duty" @@ -19,7 +20,7 @@ import ( "github.com/spf13/cobra" ) -var catalogGetSince time.Duration +var catalogGetSince string var Get = &cobra.Command{ Use: "get ", @@ -48,6 +49,17 @@ var Get = &cobra.Command{ } func resolveConfigID(ctx context.Context, args []string) (*models.ConfigItem, error) { + configs, err := resolveConfigs(ctx, args, 2) + if err != nil { + return nil, err + } + if len(configs) > 1 { + return nil, fmt.Errorf("query matched multiple configs, expected exactly one") + } + return &configs[0], nil +} + +func resolveConfigs(ctx context.Context, args []string, limit int) ([]models.ConfigItem, error) { if id, err := uuid.Parse(args[0]); err == nil { config, err := query.GetCachedConfig(ctx, id.String()) if err != nil { @@ -56,11 +68,13 @@ func resolveConfigID(ctx context.Context, args []string) (*models.ConfigItem, er if config == nil { return nil, fmt.Errorf("config item %s not found", id) } - return config, nil + return []models.ConfigItem{*config}, nil } req := parseQuery(args) - req.Limit = 2 + if limit > 0 { + req.Limit = limit + } response, err := query.SearchResources(ctx, req) if err != nil { return nil, fmt.Errorf("search failed: %w", err) @@ -68,18 +82,21 @@ func resolveConfigID(ctx context.Context, args []string) (*models.ConfigItem, er if len(response.Configs) == 0 { return nil, fmt.Errorf("no config found matching query") } - if len(response.Configs) > 1 { - return nil, fmt.Errorf("query matched multiple configs, expected exactly one") - } - config, err := query.GetCachedConfig(ctx, response.Configs[0].ID) - if err != nil { - return nil, fmt.Errorf("failed to get config: %w", err) + var configs []models.ConfigItem + for _, c := range response.Configs { + config, err := query.GetCachedConfig(ctx, c.ID) + if err != nil { + return nil, fmt.Errorf("failed to get config %s: %w", c.ID, err) + } + if config != nil { + configs = append(configs, *config) + } } - if config == nil { - return nil, fmt.Errorf("config item not found") + if len(configs) == 0 { + return nil, fmt.Errorf("no config found matching query") } - return config, nil + return configs, nil } type CatalogGetResult struct { @@ -91,19 +108,23 @@ type CatalogGetResult struct { Access []models.ConfigAccessSummary `json:"access,omitempty"` PlaybookRuns []models.PlaybookRun `json:"playbook_runs,omitempty"` - since time.Duration + since string } -func runCatalogGet(ctx context.Context, args []string, since time.Duration) (*CatalogGetResult, error) { +func runCatalogGet(ctx context.Context, args []string, sinceStr string) (*CatalogGetResult, error) { config, err := resolveConfigID(ctx, args) if err != nil { return nil, err } - sinceTime := time.Now().Add(-since) + since, err := duration.ParseDuration(sinceStr) + if err != nil { + return nil, fmt.Errorf("invalid --since value %q: %w", sinceStr, err) + } + sinceTime := time.Now().Add(-time.Duration(since)) id := config.ID - result := &CatalogGetResult{ConfigItem: *config, since: since} + result := &CatalogGetResult{ConfigItem: *config, since: sinceStr} var lastScraped models.ConfigItemLastScrapedTime if err := ctx.DB().Where("config_id = ?", id).First(&lastScraped).Error; err == nil { @@ -115,16 +136,41 @@ func runCatalogGet(ctx context.Context, args []string, since time.Duration) (*Ca return nil, fmt.Errorf("failed to get related configs: %w", err) } - if err := ctx.DB().Where("config_id = ? AND status = 'open'", id). - Find(&result.Insights).Error; err != nil { - return nil, fmt.Errorf("failed to get insights: %w", err) + changesResp, err := query.FindCatalogChanges(ctx, query.CatalogChangesSearchRequest{ + BaseCatalogSearch: query.BaseCatalogSearch{ + CatalogID: id.String(), + FromTime: &sinceTime, + PageSize: 50, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to get changes: %w", err) + } + result.Changes = make([]models.ConfigChange, len(changesResp.Changes)) + for i, c := range changesResp.Changes { + result.Changes[i] = models.ConfigChange{ + ID: c.ID, + ConfigID: c.ConfigID, + ChangeType: c.ChangeType, + Severity: models.Severity(c.Severity), + Source: c.Source, + Summary: c.Summary, + Count: c.Count, + CreatedAt: c.CreatedAt, + CreatedBy: c.CreatedBy, + } } - if err := ctx.DB().Where("config_id = ? AND created_at >= ?", id, sinceTime). - Order("created_at DESC").Limit(50). - Find(&result.Changes).Error; err != nil { - return nil, fmt.Errorf("failed to get changes: %w", err) + insightsResp, err := query.FindCatalogInsights(ctx, query.CatalogInsightsSearchRequest{ + BaseCatalogSearch: query.BaseCatalogSearch{ + CatalogID: id.String(), + PageSize: 50, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to get insights: %w", err) } + result.Insights = insightsResp.Insights result.Access, err = query.FindConfigAccessByConfigIDs(ctx, []uuid.UUID{id}) if err != nil { @@ -156,7 +202,7 @@ func (r CatalogGetResult) Pretty() api.Text { )) } - sinceLabel := formatDuration(r.since) + sinceLabel := r.since if len(r.Insights) > 0 { rows := lo.Map(r.Insights, func(a models.ConfigAnalysis, _ int) analysisRow { diff --git a/cmd/catalog_report.go b/cmd/catalog_report.go new file mode 100644 index 000000000..91e647933 --- /dev/null +++ b/cmd/catalog_report.go @@ -0,0 +1,144 @@ +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/flanksource/commons/duration" + "github.com/flanksource/commons/logger" + "github.com/flanksource/commons/properties" + "github.com/flanksource/duty" + "github.com/flanksource/duty/shutdown" + "github.com/spf13/cobra" + + "github.com/flanksource/incident-commander/api" + "github.com/flanksource/incident-commander/catalog_report" + "github.com/flanksource/incident-commander/report" +) + +var ( + catalogReportFormat string + catalogReportOutFile string + catalogReportSince string + catalogReportTitle string + + catalogReportChanges bool + catalogReportInsights bool + catalogReportRelationships bool + catalogReportAccess bool + catalogReportAccessLogs bool + catalogReportConfigJSON bool + catalogReportRecursive bool + catalogReportGroupBy string + catalogReportChangeArtifacts bool +) + +var CatalogReportCmd = &cobra.Command{ + Use: "report ", + Short: "Generate a catalog report for a config item", + Long: `Generate a PDF/HTML report for a config item including changes, insights, +relationships, RBAC access, and access logs. + +Examples: + # By config ID + catalog report 018f4e6a-1234-5678-9abc-def012345678 + + # By query + catalog report type=Kubernetes::Namespace name=default + + # HTML output + catalog report 018f4e6a-... --format facet-html -o report.html + + # JSON with config body included + catalog report 018f4e6a-... --format json --config-json`, + Args: cobra.MinimumNArgs(1), + PersistentPreRun: PreRun, + RunE: func(cmd *cobra.Command, args []string) error { + logger.UseSlog() + if err := properties.LoadFile("mission-control.properties"); err != nil { + logger.Errorf(err.Error()) + } + + ctx, stop, err := duty.Start("mission-control", duty.ClientOnly) + if err != nil { + return err + } + shutdown.AddHookWithPriority("database", shutdown.PriorityCritical, stop) + shutdown.WaitForSignal() + + configs, err := resolveConfigs(ctx, args, 0) + if err != nil { + return err + } + + opts := buildCatalogReportOptions() + + data, err := catalog_report.Export(ctx, configs, opts, catalogReportFormat) + if err != nil { + shutdown.ShutdownAndExit(1, err.Error()) + return err + } + + out := catalogReportOutFile + if out == "" { + out = "stdout" + } + logger.Infof("Rendering catalog report to %s (%s) %dKB", out, catalogReportFormat, len(data)/1024) + + if catalogReportOutFile != "" { + if err := os.WriteFile(catalogReportOutFile, data, 0600); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + } else { + fmt.Print(string(data)) + } + + return nil + }, +} + +func buildCatalogReportOptions() catalog_report.Options { + opts := catalog_report.Options{ + Title: catalogReportTitle, + Recursive: catalogReportRecursive, + GroupBy: catalogReportGroupBy, + ChangeArtifacts: catalogReportChangeArtifacts, + Sections: api.CatalogReportSections{ + Changes: catalogReportChanges, + Insights: catalogReportInsights, + Relationships: catalogReportRelationships, + Access: catalogReportAccess, + AccessLogs: catalogReportAccessLogs, + ConfigJSON: catalogReportConfigJSON, + }, + } + + if catalogReportSince != "" { + if d, err := duration.ParseDuration(catalogReportSince); err == nil { + opts.Since = time.Duration(d) + } + } + + return opts +} + +func init() { + CatalogReportCmd.Flags().StringVarP(&catalogReportFormat, "format", "f", "facet-pdf", "Output format: json, facet-html, facet-pdf") + CatalogReportCmd.Flags().StringVarP(&catalogReportOutFile, "out-file", "o", "", "Write output to file instead of stdout") + CatalogReportCmd.Flags().StringVar(&catalogReportSince, "since", "30d", "Time range for changes and access logs (supports d/w/y e.g. 7d, 2w, 30d)") + CatalogReportCmd.Flags().StringVar(&catalogReportTitle, "title", "", "Report title (default auto-generated)") + CatalogReportCmd.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory or TSX file for report rendering (overrides embedded reports)") + + CatalogReportCmd.Flags().BoolVar(&catalogReportRecursive, "recursive", false, "Include all descendant config items") + CatalogReportCmd.Flags().StringVar(&catalogReportGroupBy, "group-by", "merged", "Group descendant data: 'merged' or 'config'") + CatalogReportCmd.Flags().BoolVar(&catalogReportChangeArtifacts, "change-artifacts", false, "Embed change artifacts (images/screenshots) in the report") + CatalogReportCmd.Flags().BoolVar(&catalogReportChanges, "changes", true, "Include config changes section") + CatalogReportCmd.Flags().BoolVar(&catalogReportInsights, "insights", true, "Include config insights section") + CatalogReportCmd.Flags().BoolVar(&catalogReportRelationships, "relationships", true, "Include relationships section") + CatalogReportCmd.Flags().BoolVar(&catalogReportAccess, "access", true, "Include RBAC access section") + CatalogReportCmd.Flags().BoolVar(&catalogReportAccessLogs, "access-logs", true, "Include access logs section") + CatalogReportCmd.Flags().BoolVar(&catalogReportConfigJSON, "config-json", false, "Include raw config JSON") + + Catalog.AddCommand(CatalogReportCmd) +} diff --git a/cmd/catalog_tree.go b/cmd/catalog_tree.go new file mode 100644 index 000000000..7cf4be978 --- /dev/null +++ b/cmd/catalog_tree.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "github.com/flanksource/clicky" + "github.com/flanksource/clicky/api" + "github.com/flanksource/commons/logger" + "github.com/flanksource/commons/properties" + "github.com/flanksource/duty" + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/query" + "github.com/samber/lo" + "github.com/spf13/cobra" +) + +var ( + treeDirection string + treeSoft bool + treeHealth bool +) + +var Tree = &cobra.Command{ + Use: "tree ", + Short: "Show a config item's parent/child hierarchy and relationships as a tree", + Long: `Display config item hierarchy (parents + children) and relationships as a tree. + +Parent/child edges are shown normally. Relationship edges are marked with ~. + +Examples: + catalog tree 018f4e6a-1234-5678-9abc-def012345678 + catalog tree type=Kubernetes::Namespace name=default + catalog tree --direction=incoming + catalog tree --direction=outgoing --soft`, + Args: cobra.MinimumNArgs(1), + PersistentPreRun: PreRun, + RunE: func(cmd *cobra.Command, args []string) error { + logger.UseSlog() + if err := properties.LoadFile("mission-control.properties"); err != nil { + logger.Errorf(err.Error()) + } + ctx, stop, err := duty.Start("mission-control", duty.ClientOnly) + if err != nil { + return err + } + defer stop() + + result, err := runCatalogTree(ctx, args) + if err != nil { + return err + } + + clicky.MustPrint(result, clicky.Flags.FormatOptions) + return nil + }, +} + +type CatalogTreeResult struct { + *query.ConfigTreeNode +} + +func (r CatalogTreeResult) Pretty() api.Text { + return treeNodeLabel(r.ConfigTreeNode) +} + +func (r CatalogTreeResult) GetChildren() []api.TreeNode { + nodes := make([]api.TreeNode, len(r.Children)) + for i, c := range r.Children { + nodes[i] = treeNodeAdapter{c} + } + return nodes +} + +type treeNodeAdapter struct { + *query.ConfigTreeNode +} + +func (n treeNodeAdapter) Pretty() api.Text { + return treeNodeLabel(n.ConfigTreeNode) +} + +func (n treeNodeAdapter) GetChildren() []api.TreeNode { + nodes := make([]api.TreeNode, len(n.Children)) + for i, c := range n.Children { + nodes[i] = treeNodeAdapter{c} + } + return nodes +} + +func treeNodeLabel(n *query.ConfigTreeNode) api.Text { + isTarget := n.EdgeType == "target" + isRelated := n.EdgeType == "related" + + t := clicky.Text("") + if isRelated { + t = t.AddText("~ ", "text-purple-500") + } + if n.Type != nil { + t = t.Add(clicky.Text(lo.FromPtr(n.Type), "text-xs text-gray-600")) + t = t.AddText("/") + } + style := "font-bold" + if isTarget { + style = "font-bold underline" + } + t = t.AddText(lo.FromPtrOr(n.Name, ""), style) + if isRelated && n.Relation != "" { + t = t.AddText(" ").Add(clicky.Text(n.Relation, "text-xs text-purple-400 italic")) + } + if treeHealth { + if n.Health != nil { + t = t.AddText(" ").Add(n.Health.Pretty()) + } + if n.Status != nil && *n.Status != "" { + t = t.AddText(" ").Add(clicky.Text(*n.Status, "text-xs text-gray-500")) + } + } + if clicky.Flags.LevelCount >= 2 { + t = t.AddText(" ").Add(clicky.Text(n.ID.String(), "text-xs font-mono text-gray-400")) + if n.Path != "" { + t = t.AddText(" ").Add(clicky.Text(n.Path, "text-xs text-gray-400")) + } + } + return t +} + +func runCatalogTree(ctx context.Context, args []string) (*CatalogTreeResult, error) { + config, err := resolveConfigID(ctx, args) + if err != nil { + return nil, err + } + + relType := query.Hard + if treeSoft { + relType = query.Both + } + + tree, err := query.ConfigTree(ctx, config.ID, query.ConfigTreeOptions{ + Direction: query.RelationDirection(treeDirection), + Incoming: relType, + Outgoing: relType, + }) + if err != nil { + return nil, err + } + + return &CatalogTreeResult{ConfigTreeNode: tree}, nil +} + +func init() { + Tree.Flags().StringVar(&treeDirection, "direction", "all", "Relationship direction: all, incoming, outgoing") + Tree.Flags().BoolVar(&treeSoft, "soft", false, "Include soft relationships") + Tree.Flags().BoolVar(&treeHealth, "health", false, "Show health and status") + clicky.BindAllFlags(Tree.PersistentFlags(), "format") + Catalog.AddCommand(Tree) +} diff --git a/rbac_report/export.go b/rbac_report/export.go index 5ee122d63..96dcfec31 100644 --- a/rbac_report/export.go +++ b/rbac_report/export.go @@ -23,14 +23,14 @@ func Export(ctx context.Context, opts Options, format string) ([]byte, error) { switch format { case "csv": - if opts.ByUser { + if opts.View == "user" { return renderCSVByUser(report) } return renderCSV(report) - case "facet-html": - return RenderFacetHTML(ctx, report, opts.ByUser) - case "facet-pdf": - return RenderFacetPDF(ctx, report, opts.ByUser) + case "html", "facet-html": + return RenderFacetHTML(ctx, report, opts.View) + case "pdf", "facet-pdf": + return RenderFacetPDF(ctx, report, opts.View) default: return json.MarshalIndent(report, "", " ") } diff --git a/rbac_report/render_facet.go b/rbac_report/render_facet.go index 54cd192dc..33bef171d 100644 --- a/rbac_report/render_facet.go +++ b/rbac_report/render_facet.go @@ -4,26 +4,28 @@ import ( "fmt" "github.com/flanksource/duty/context" + "github.com/flanksource/duty/models" "github.com/flanksource/incident-commander/api" "github.com/flanksource/incident-commander/report" ) -func RenderFacetHTML(ctx context.Context, r *api.RBACReport, byUser bool) ([]byte, error) { - return renderWithFacet(ctx, r, "html", byUser) +func RenderFacetHTML(ctx context.Context, r *api.RBACReport, view string) ([]byte, error) { + return renderWithFacet(ctx, r, "html", view) } -func RenderFacetPDF(ctx context.Context, r *api.RBACReport, byUser bool) ([]byte, error) { - return renderWithFacet(ctx, r, "pdf", byUser) +func RenderFacetPDF(ctx context.Context, r *api.RBACReport, view string) ([]byte, error) { + return renderWithFacet(ctx, r, "pdf", view) } -func renderWithFacet(ctx context.Context, r *api.RBACReport, format string, byUser bool) ([]byte, error) { +func renderWithFacet(ctx context.Context, r *api.RBACReport, format string, view string) ([]byte, error) { if r == nil { return nil, fmt.Errorf("RBAC report must not be nil") } - entryFile := "RBACReport.tsx" - if byUser { + entryFile := "RBACMatrixReport.tsx" + switch view { + case "user": entryFile = "RBACByUserReport.tsx" } @@ -40,6 +42,9 @@ func renderWithFacet(ctx context.Context, r *api.RBACReport, format string, byUs func initSlices(r *api.RBACReport) api.RBACReport { out := *r + if out.Parents == nil { + out.Parents = []models.ConfigItem{} + } if out.Resources == nil { out.Resources = []api.RBACResource{} } diff --git a/rbac_report/report.go b/rbac_report/report.go index 70f4a1737..da608b86b 100644 --- a/rbac_report/report.go +++ b/rbac_report/report.go @@ -23,7 +23,7 @@ type Options struct { StaleDays int ReviewOverdueDays int ChangelogSince time.Duration - ByUser bool + View string } func (o Options) WithDefaults() Options { @@ -81,16 +81,20 @@ func BuildReport(ctx context.Context, opts Options) (*api.RBACReport, error) { ctx.Logger.V(3).Infof("Changelog: %d entries, temporary access: %d entries", len(changelog), len(tempAccess)) + subject, parents := resolveSubjectAndParents(ctx, configItems, configMap) + report := &api.RBACReport{ Title: opts.Title, Query: formatSelectors(opts.Selectors), GeneratedAt: time.Now(), + Subject: subject, + Parents: parents, Resources: resources, Changelog: changelog, Summary: summary, } - if opts.ByUser { + if opts.View == "user" { report.Users = groupByUser(rows, opts, configMap) } @@ -188,7 +192,43 @@ func groupByConfigItem(rows []db.RBACAccessRow, opts Options, configMap map[stri }) } +func resolveSubjectAndParents(ctx context.Context, configItems []models.ConfigItem, configMap map[string]models.ConfigItem) (*models.ConfigItem, []models.ConfigItem) { + if len(configItems) == 0 { + return nil, nil + } + + first := configItems[0] + + var parents []models.ConfigItem + current := first + for current.ParentID != nil { + parent, ok := configMap[current.ParentID.String()] + if !ok { + loaded, err := query.GetConfigsByIDs(ctx, []uuid.UUID{*current.ParentID}) + if err != nil || len(loaded) == 0 { + break + } + parent = loaded[0] + configMap[parent.ID.String()] = parent + } + parents = append(parents, parent) + current = parent + } + + // Reverse so root is first + for i, j := 0, len(parents)-1; i < j; i, j = i+1, j-1 { + parents[i], parents[j] = parents[j], parents[i] + } + + return &first, parents +} + func enrichResourceFromConfigItem(resource *api.RBACResource, ci models.ConfigItem) { + resource.ConfigClass = ci.ConfigClass + resource.Path = ci.Path + if ci.ParentID != nil { + resource.ParentID = ci.ParentID.String() + } if ci.Status != nil { resource.Status = *ci.Status } @@ -259,6 +299,8 @@ func groupByUser(rows []db.RBACAccessRow, opts Options, configMap map[string]mod IsReviewOverdue: isReviewOverdue, } if ci, found := configMap[row.ConfigID.String()]; found { + res.ConfigClass = ci.ConfigClass + res.Path = ci.Path if ci.Status != nil { res.Status = *ci.Status } diff --git a/report/Application.tsx b/report/Application.tsx index a0f1b9137..278add9da 100644 --- a/report/Application.tsx +++ b/report/Application.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Page, PageBreak } from '@flanksource/facet'; +import { Document, Page, Header, Footer } from '@flanksource/facet'; import type { Application } from './types.ts'; import ApplicationDetails from './components/ApplicationDetails.tsx'; import AccessControlSection from './components/AccessControlSection.tsx'; @@ -9,26 +9,8 @@ import FindingsSection from './components/FindingsSection.tsx'; import LocationsSection from './components/LocationsSection.tsx'; import DynamicSection from './components/DynamicSection.tsx'; import CoverPage from './components/CoverPage.tsx'; - -function PageHeader({ app }: { app: Application }) { - return ( -
- {app.name} - Application Report -
- ); -} - -function PageFooter() { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric' - }); - return ( -
- Generated {date} -
- ); -} +import PageHeader from './components/PageHeader.tsx'; +import PageFooter from './components/PageFooter.tsx'; function AppCoverPage({ app }: { app: Application }) { return ( @@ -46,87 +28,32 @@ interface ApplicationReportProps { } export default function ApplicationReport({ data }: ApplicationReportProps) { - const header = ; - const footer = ; - const pageProps = { - pageSize: 'a4' as const, - margins: { top: 5, bottom: 5, left: 10, right: 10 }, - header, - headerHeight: 10, - footer, - footerHeight: 10, - }; - return ( - <> - {/* Cover page — no header/footer */} - + +
+ +
+
+ +
+ + - - - {/* Application details + properties */} - + - - - - - {/* Access control */} - + + {(data.backups.length > 0 || data.restores.length > 0) && ( + + )} + + {data.sections.map((section, idx) => ( + + ))} + - - {/* Incidents */} - {data.incidents.length > 0 && ( - <> - - - - - - )} - - {/* Backups & Restores */} - {(data.backups.length > 0 || data.restores.length > 0) && ( - <> - - - - - - )} - - {/* Security findings */} - {data.findings.length > 0 && ( - <> - - - - - - )} - - {/* Dynamic sections (view / changes / configs) */} - {data.sections.map((section, idx) => ( - - - - - - - ))} - - {/* Locations */} - {data.locations.length > 0 && ( - <> - - - - - - )} - +
); } diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index 1892d94ef..773abc901 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Page, PageBreak, Section } from '@flanksource/facet'; +import { Document, Page, Header, Footer, Section } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { CatalogReportData, CatalogReportConfigGroup } from './catalog-report-types.ts'; import ConfigChangesSection from './components/ConfigChangesSection.tsx'; @@ -12,24 +12,8 @@ import RBACMatrixSection from './components/RBACMatrixSection.tsx'; import ArtifactAppendix from './components/ArtifactAppendix.tsx'; import CoverPage from './components/CoverPage.tsx'; import CatalogList from './components/CatalogList.tsx'; - -function PageHeader({ title }: { title: string }) { - return ( -
- {title} - Catalog Report -
- ); -} - -function PageFooter() { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - return ( -
- Generated {now} -
- ); -} +import PageHeader from './components/PageHeader.tsx'; +import PageFooter from './components/PageFooter.tsx'; function CatalogCoverPage({ data }: { data: CatalogReportData }) { const ci = data.configItem || {}; @@ -94,17 +78,6 @@ interface CatalogReportProps { } export default function CatalogReportPage({ data }: CatalogReportProps) { - const header = ; - const footer = ; - const pageProps = { - pageSize: 'a4' as const, - margins: { top: 1, bottom: 1, left: 5, right: 5 }, - header, - headerHeight: 8, - footer, - footerHeight: 8, - }; - const configItem = { id: data.configItem.id, name: data.configItem.name, @@ -120,154 +93,65 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { }; return ( - <> - + +
+ +
+
+ +
+ + - {(data.entries || []).length > 0 && ( - <> - - - - - - )} - - {data.groupBy === 'config' && (data.entries || []).map((entry, idx) => ( - - {(entry.changes || []).length > 0 && ( - <> - - - - - - - )} - {(entry.analyses || []).length > 0 && ( - <> - - - - - - - )} - {(entry.rbacResources || []).length > 0 && ( - <> - - - - {entry.rbacResources!.map((resource, rIdx) => ( - - ))} - - - )} - - ))} - - {data.groupBy !== 'config' && data.sections?.changes && (data.changes || []).length > 0 && ( - <> - - + + + + {data.groupBy === 'config' && (data.entries || []).map((entry, idx) => ( + + + + + {(entry.rbacResources || []).map((resource, rIdx) => ( + + ))} + + ))} + + {data.groupBy !== 'config' && ( + <> - - - )} - - {data.groupBy !== 'config' && data.sections?.insights && (data.analyses || []).length > 0 && ( - <> - - - - - )} - - {data.sections?.relationships && data.relationshipTree && ( - <> - - - - - - )} + + )} - {data.sections?.relationships && !data.relationshipTree && (data.relatedConfigs || []).length > 0 && ( - <> - - - - - - )} + {data.relationshipTree + ? + : + } - {data.groupBy !== 'config' && data.sections?.access && (data.access || []).length > 0 && ( - <> - - + {data.groupBy !== 'config' && ( + <> - - - )} - - {data.groupBy !== 'config' && data.sections?.accessLogs && (data.accessLogs || []).length > 0 && ( - <> - - - - - )} + + )} - {data.groupBy === 'config' && (data.configGroups || []).map((group, idx) => ( - - + {data.groupBy === 'config' && (data.configGroups || []).map((group, idx) => ( + - {(group.changes || []).length > 0 && ( - - )} - {(group.analyses || []).length > 0 && ( - - )} - {(group.access || []).length > 0 && ( - - )} - {(group.accessLogs || []).length > 0 && ( - - )} - - - ))} + + + + + + ))} - {data.sections?.configJSON && data.configJSON && ( - <> - - - - - - )} + {data.configJSON && } - {(() => { - const allChanges = (data.entries || []).flatMap((e) => e.changes || []); - const withArtifacts = allChanges.filter((c) => (c.artifacts || []).length > 0); - if (withArtifacts.length === 0) return null; - return ( - <> - - - - - - ); - })()} - + e.changes || [])} /> + +
); } diff --git a/report/MatrixDemo.tsx b/report/MatrixDemo.tsx index 7882711c4..274ee5b43 100644 --- a/report/MatrixDemo.tsx +++ b/report/MatrixDemo.tsx @@ -36,16 +36,16 @@ export default function MatrixDemo() { return ( -

+

RBAC Matrix - Visual System Demo

{/* --- Reference Section --- */}
-
+
Identity Types
-
+
{(['user', 'group', 'service', 'bot'] as const).map((type) => (
-
+
Access Pattern - Filled vs Unfilled
-
+
Direct (filled) @@ -77,7 +77,7 @@ export default function MatrixDemo() {
{/* --- Matrix 1: Simple Permissions --- */} -
+
Simple Permissions
{/* --- Matrix 2: Database Roles --- */} -
+
Database Roles
{(() => { @@ -189,7 +189,7 @@ export default function MatrixDemo() { })()} {/* --- Legend --- */} -
+
Access: Direct diff --git a/report/RBACByUserReport.tsx b/report/RBACByUserReport.tsx index e1dc75645..6fb7905c0 100644 --- a/report/RBACByUserReport.tsx +++ b/report/RBACByUserReport.tsx @@ -1,80 +1,43 @@ import React from 'react'; -import { Page, PageBreak } from '@flanksource/facet'; +import { Document, Page, Header, Footer } from '@flanksource/facet'; import type { RBACReport } from './rbac-types.ts'; import RBACSummarySection from './components/RBACSummarySection.tsx'; import RBACUserSection from './components/RBACUserSection.tsx'; import RBACChangelogSection from './components/RBACChangelogSection.tsx'; import RBACCoverContent from './components/RBACCoverContent.tsx'; import { MatrixLegend } from './components/RBACMatrixSection.tsx'; - -function PageHeader({ title }: { title: string }) { - return ( -
- {title} - RBAC Report (By User) -
- ); -} - -function PageFooter() { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - return ( -
- -
- Generated {now} -
-
- ); -} +import PageHeader from './components/PageHeader.tsx'; +import PageFooter from './components/PageFooter.tsx'; interface Props { data: RBACReport; } export default function RBACByUserReportPage({ data }: Props) { - const header = ; - const footer = ; - const pageProps = { - pageSize: 'a4-landscape' as const, - margins: { top: 1, bottom: 1, left: 5, right: 5 }, - header, - headerHeight: 8, - footer, - footerHeight: 14, - }; - const users = data.users || []; return ( - <> - + +
+ +
+
+ +
+ + - - - + - - {users.map((user, idx) => ( - - - - - - - ))} + {users.map((user, idx) => ( + + ))} - {data.changelog.length > 0 && ( - <> - - - - - - )} - + + +
); } diff --git a/report/RBACMatrixReport.tsx b/report/RBACMatrixReport.tsx index 2b36a6609..82aea2312 100644 --- a/report/RBACMatrixReport.tsx +++ b/report/RBACMatrixReport.tsx @@ -1,34 +1,15 @@ import React from 'react'; -import { Page, PageBreak } from '@flanksource/facet'; +import { Document, Page, Header, Footer } from '@flanksource/facet'; import type { RBACReport, RBACResource } from './rbac-types.ts'; import RBACSummarySection from './components/RBACSummarySection.tsx'; import RBACMatrixSection, { MatrixLegend } from './components/RBACMatrixSection.tsx'; import RBACChangelogSection from './components/RBACChangelogSection.tsx'; import RBACCoverContent from './components/RBACCoverContent.tsx'; - -function PageHeader({ title }: { title: string }) { - return ( -
- {title} - RBAC Matrix -
- ); -} - -function PageFooter() { - const now = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - return ( -
- -
- Generated {now} -
-
- ); -} +import PageHeader from './components/PageHeader.tsx'; +import PageFooter from './components/PageFooter.tsx'; function estimateHeight(resource: RBACResource): number { - const uniqueUsers = new Set(resource.users.map((u) => u.userId)).size; + const uniqueUsers = new Set((resource.users || []).map((u) => u.userId)).size; return 20 + uniqueUsers * 3 + 5; } @@ -57,52 +38,34 @@ interface RBACMatrixReportProps { } export default function RBACMatrixReportPage({ data }: RBACMatrixReportProps) { - const header = ; - const footer = ; - const pageProps = { - pageSize: 'a4-landscape' as const, - margins: { top: 1, bottom: 1, left: 5, right: 5 }, - header, - headerHeight: 8, - footer, - footerHeight: 14, - }; - - const resourcePages = packResources(data.resources, 160); + const resourcePages = packResources(data.resources || [], 160); return ( - <> - + +
+ +
+
+ +
+ + - - - + - - {resourcePages.map((group, pageIdx) => ( - - - -
- {group.map((resource, idx) => ( - - ))} -
-
-
- ))} + {resourcePages.map((group, pageIdx) => ( +
+ {group.map((resource, idx) => ( + + ))} +
+ ))} - {data.changelog.length > 0 && ( - <> - - - - - - )} - + +
+
); } diff --git a/report/ViewReport.tsx b/report/ViewReport.tsx index 5e851425d..47ed3ad6b 100644 --- a/report/ViewReport.tsx +++ b/report/ViewReport.tsx @@ -1,32 +1,10 @@ import React from 'react'; -import { Page, PageBreak } from '@flanksource/facet'; -import { Icon } from '@flanksource/icons/icon'; +import { Document, Page, Header, Footer } from '@flanksource/facet'; import type { ViewReportData, MultiViewReportData } from './view-types.ts'; import ViewResultSection from './components/ViewResultSection.tsx'; import CoverPage from './components/CoverPage.tsx'; - -function PageHeader({ title, icon }: { title: string; icon?: string }) { - return ( -
- - {icon && } - {title} - - View Report -
- ); -} - -function PageFooter() { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); - return ( -
- Generated {date} -
- ); -} +import PageHeader from './components/PageHeader.tsx'; +import PageFooter from './components/PageFooter.tsx'; function ViewCoverPage({ data }: { data: ViewReportData }) { const variableTags = (data.variables || []).reduce((acc, v) => { @@ -56,33 +34,26 @@ export default function ViewReportPage({ data }: ViewReportProps) { const viewsList = isMultiView(data) ? data.views : [data]; const firstView = viewsList[0]; - const header = ; - const footer = ; - const pageProps = { - pageSize: 'a4' as const, - margins: { top: 3, bottom: 3, left: 10, right: 10 }, - header, - headerHeight: 10, - footer, - footerHeight: 10, - }; - return ( - <> - + +
+ +
+
+ +
+ + {viewsList.map((view, idx) => ( - - - -
- -
-
-
+ +
+ +
+
))} - +
); } diff --git a/report/catalog-report-types.ts b/report/catalog-report-types.ts index 41a245337..6f9efc623 100644 --- a/report/catalog-report-types.ts +++ b/report/catalog-report-types.ts @@ -56,6 +56,7 @@ export interface CatalogReportConfigGroup { export interface CatalogReportData { title: string; generatedAt: string; + publicURL?: string; from?: string; to?: string; recursive?: boolean; diff --git a/report/components/ArtifactAppendix.tsx b/report/components/ArtifactAppendix.tsx new file mode 100644 index 000000000..f23762211 --- /dev/null +++ b/report/components/ArtifactAppendix.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Section } from '@flanksource/facet'; +import { Icon } from '@flanksource/icons/icon'; +import type { ConfigChange } from '../config-types.ts'; +import { formatMonthDay, formatTime } from './utils.ts'; + +interface Props { + changes?: ConfigChange[]; +} + +export default function ArtifactAppendix({ changes }: Props) { + const withArtifacts = (changes || []).filter((c) => (c.artifacts || []).length > 0); + if (withArtifacts.length === 0) return null; + + const grouped = new Map(); + for (const c of withArtifacts) { + const key = c.configName || c.configID || 'unknown'; + if (!grouped.has(key)) { + grouped.set(key, { configName: c.configName || 'Unknown', configType: c.configType, changes: [] }); + } + grouped.get(key)!.changes.push(c); + } + + return ( +
+ {[...grouped.entries()].map(([key, group]) => ( +
+
+ {group.configType && } + {group.configName} + {group.configType && ({group.configType})} +
+ + {group.changes.map((change) => ( +
+
+ {change.createdAt ? `${formatMonthDay(change.createdAt)} ${formatTime(change.createdAt)}` : ''} + + {change.changeType} + {change.summary ?? ''} +
+ {(change.artifacts || []).map((a) => { + if (a.dataUri && a.contentType.startsWith('image/')) { + return ( + + {a.filename} +
{a.filename}
+
+ ); + } + return ( +
+ {a.filename} ({a.contentType}, {formatSize(a.size)}) +
+ ); + })} +
+ ))} +
+ ))} +
+ ); +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; +} diff --git a/report/components/CatalogAccessLogsSection.tsx b/report/components/CatalogAccessLogsSection.tsx index e574b85da..d7419371e 100644 --- a/report/components/CatalogAccessLogsSection.tsx +++ b/report/components/CatalogAccessLogsSection.tsx @@ -1,39 +1,33 @@ import React from 'react'; import { Section, CompactTable } from '@flanksource/facet'; import type { CatalogReportAccessLog } from '../catalog-report-types.ts'; -import { formatRelative } from './utils.ts'; +import { formatMonthDay, formatTime } from './utils.ts'; interface Props { - logs: CatalogReportAccessLog[]; + logs?: CatalogReportAccessLog[]; } function MFABadge({ mfa }: { mfa: boolean }) { if (mfa) { - return MFA; + return MFA; } - return no MFA; + return no MFA; } export default function CatalogAccessLogsSection({ logs }: Props) { - if (logs.length === 0) { - return ( -
-

No access log entries found.

-
- ); - } + if (!logs?.length) return null; const rows = logs.map((log) => [ {log.userName}, - log.createdAt ? formatRelative(log.createdAt) : '-', + log.createdAt ? `${formatMonthDay(log.createdAt)} ${formatTime(log.createdAt)}` : '-', , log.count > 1 ? ( - ×{log.count} + ×{log.count} ) : ( '1' ), log.properties && Object.keys(log.properties).length > 0 ? ( - + {Object.entries(log.properties).map(([k, v]) => `${k}=${v}`).join(', ')} ) : ( diff --git a/report/components/CatalogAccessSection.tsx b/report/components/CatalogAccessSection.tsx index 83cf8d9eb..c0d08e866 100644 --- a/report/components/CatalogAccessSection.tsx +++ b/report/components/CatalogAccessSection.tsx @@ -1,59 +1,64 @@ import React from 'react'; import { Section, CompactTable } from '@flanksource/facet'; import type { CatalogReportAccess } from '../catalog-report-types.ts'; -import { formatRelative } from './utils.ts'; +import { formatMonthDay } from './utils.ts'; interface Props { - access: CatalogReportAccess[]; + access?: CatalogReportAccess[]; } function StaleBadge({ lastSignedInAt }: { lastSignedInAt?: string }) { if (!lastSignedInAt) { - return never; + return never; } const days = Math.floor((Date.now() - new Date(lastSignedInAt).getTime()) / 86400000); if (days > 90) { - return stale; + return stale; } if (days > 30) { - return aging; + return aging; } return null; } export default function CatalogAccessSection({ access }: Props) { - if (access.length === 0) { - return ( -
-

No access entries found.

-
- ); - } + if (!access?.length) return null; + + const hasMultipleConfigs = access.some((a) => a.configName); + const rows = access.map((a) => { + const row = [ + {a.userName}, + a.role, + {a.email}, + {a.userType}, + a.lastSignedInAt ? ( + + {formatMonthDay(a.lastSignedInAt)} + + + ) : ( + + - + + + ), + a.lastReviewedAt ? formatMonthDay(a.lastReviewedAt) : -, + ]; + if (hasMultipleConfigs) { + row.splice(0, 0, {a.configName || '-'}); + } + return row; + }); - const rows = access.map((a) => [ - {a.userName}, - a.role, - {a.email}, - {a.userType}, - a.lastSignedInAt ? ( - - {formatRelative(a.lastSignedInAt)} - - - ) : ( - - - - - - ), - a.lastReviewedAt ? formatRelative(a.lastReviewedAt) : -, - ]); + const columns = hasMultipleConfigs + ? ['Config', 'User', 'Role', 'Email', 'Type', 'Last Sign In', 'Last Reviewed'] + : ['User', 'Role', 'Email', 'Type', 'Last Sign In', 'Last Reviewed']; return (
diff --git a/report/components/CatalogList.tsx b/report/components/CatalogList.tsx index 37ec3d9ad..0b7e66782 100644 --- a/report/components/CatalogList.tsx +++ b/report/components/CatalogList.tsx @@ -39,10 +39,11 @@ function EntryDetail({ entry }: { entry: CatalogReportEntry }) { } interface CatalogListProps { - entries: CatalogReportEntry[]; + entries?: CatalogReportEntry[]; } export default function CatalogList({ entries }: CatalogListProps) { + if (!entries?.length) return null; return (
= { info: 'text-gray-600 bg-gray-50 border-gray-200', }; -function ChangeEntry({ change }: { change: ConfigChange }) { +function ChangeEntry({ change, dateFormat, hideConfigName }: { change: ConfigChange; dateFormat: TimeBucketFormat; hideConfigName?: boolean }) { const sev = change.severity ?? 'info'; const author = change.createdBy || change.externalCreatedBy || change.source || ''; + const artifactCount = (change.artifacts || []).length; return ( -
+
- {change.createdAt ? formatRelative(change.createdAt) : '-'} + {change.createdAt ? formatEntryDate(change.createdAt, dateFormat) : '-'} {change.changeType} + {!hideConfigName && change.configName && ( + {change.configName} + )} {change.summary ?? '-'} - - {sev} - - {author && {author}} + {sev !== 'info' && ( + + {sev} + + )} + {author && {author}} {(change.count ?? 0) > 1 && ( - ×{change.count} + ×{change.count} + )} + {artifactCount > 0 && ( + + {artifactCount} screenshot{artifactCount > 1 ? 's' : ''} → + )}
); } -export default function ConfigChangesSection({ changes }: Props) { +interface BucketGroup { + key: string; + label: string; + dateFormat: TimeBucketFormat; + changes: ConfigChange[]; +} + +function groupByTimeBucket(changes: ConfigChange[]): BucketGroup[] { + const sorted = [...changes].sort((a, b) => { + const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0; + return tb - ta; + }); + + const groups: BucketGroup[] = []; + const groupMap = new Map(); + + for (const c of sorted) { + const bucket = c.createdAt ? getTimeBucket(c.createdAt) : { key: 'unknown', label: 'Unknown', dateFormat: 'monthDay' as TimeBucketFormat }; + let group = groupMap.get(bucket.key); + if (!group) { + group = { key: bucket.key, label: bucket.label, dateFormat: bucket.dateFormat, changes: [] }; + groupMap.set(bucket.key, group); + groups.push(group); + } + group.changes.push(c); + } + + return groups; +} + +export default function ConfigChangesSection({ changes, hideConfigName: hideConfigNameProp }: Props) { + if (!changes?.length) return null; + const uniqueConfigs = new Set(changes.map((c) => c.configID || c.configName).filter(Boolean)); + const hideConfigName = hideConfigNameProp || uniqueConfigs.size <= 1; const bySeverity = Object.fromEntries( SEVERITY_ORDER.map((sev) => [sev, changes.filter((c) => (c.severity ?? 'info') === sev).length]) ); + const groups = groupByTimeBucket(changes); + return (
- {SEVERITY_ORDER.map((sev) => ( + {SEVERITY_ORDER.filter((sev) => bySeverity[sev] > 0).map((sev) => ( ))}
- {changes.length === 0 ? ( -

No changes recorded.

- ) : ( -
- {changes.map((c) => )} + {groups.map((group) => ( +
+
+ {group.label} + ({group.changes.length}) +
+
+ {group.changes.map((c) => )} +
- )} + ))}
); } diff --git a/report/components/ConfigInsightsSection.tsx b/report/components/ConfigInsightsSection.tsx index ba1ef9818..ebe810546 100644 --- a/report/components/ConfigInsightsSection.tsx +++ b/report/components/ConfigInsightsSection.tsx @@ -5,7 +5,7 @@ import type { ConfigAnalysis, ConfigSeverity, AnalysisType } from '../config-typ import { formatDate } from './utils.ts'; interface Props { - analyses: ConfigAnalysis[]; + analyses?: ConfigAnalysis[]; } const SEVERITY_ORDER: ConfigSeverity[] = ['critical', 'high', 'medium', 'low', 'info']; @@ -37,22 +37,25 @@ const ANALYSIS_TYPES: AnalysisType[] = [ function InsightEntry({ analysis }: { analysis: ConfigAnalysis }) { const sev = analysis.severity ?? 'info'; return ( -
+
{analysis.analyzer} + {analysis.configName && ( + {analysis.configName} + )} {analysis.message || analysis.summary || '-'} - + {sev} {analysis.status && ( - + {analysis.status} )} {analysis.lastObserved && ( - {formatDate(analysis.lastObserved)} + {formatDate(analysis.lastObserved)} )}
); @@ -71,8 +74,8 @@ function AnalysisTypeGroup({ type, analyses }: { type: string; analyses: ConfigA return (
- {type} - + {type} + {analyses.length}
@@ -84,6 +87,7 @@ function AnalysisTypeGroup({ type, analyses }: { type: string; analyses: ConfigA } export default function ConfigInsightsSection({ analyses }: Props) { + if (!analyses?.length) return null; const bySeverity = Object.fromEntries( SEVERITY_ORDER.map((sev) => [sev, analyses.filter((a) => (a.severity ?? 'info') === sev).length]) ); @@ -100,17 +104,12 @@ export default function ConfigInsightsSection({ analyses }: Props) { color={SEVERITY_COLOR[sev]} value={bySeverity[sev]} label={sev.charAt(0).toUpperCase() + sev.slice(1)} - size="xs" /> ))}
- {analyses.length === 0 ? ( -

No analysis results.

- ) : ( - ANALYSIS_TYPES.map((type) => ( - - )) - )} + {ANALYSIS_TYPES.map((type) => ( + + ))}
); } diff --git a/report/components/ConfigRelationshipGraph.tsx b/report/components/ConfigRelationshipGraph.tsx index 6eb8d91c8..9f7c8b9e2 100644 --- a/report/components/ConfigRelationshipGraph.tsx +++ b/report/components/ConfigRelationshipGraph.tsx @@ -6,8 +6,8 @@ import ConfigLink from './ConfigLink.tsx'; interface Props { centralConfig: ConfigItem; - relationships: ConfigRelationship[]; - relatedConfigs: ConfigItem[]; + relationships?: ConfigRelationship[]; + relatedConfigs?: ConfigItem[]; } function HealthDot({ health }: { health: string }) { @@ -54,10 +54,12 @@ function RelationshipGroup({ title, relationships, configLookup }: { } export default function ConfigRelationshipGraph({ centralConfig, relationships, relatedConfigs }: Props) { + if (!relatedConfigs?.length) return null; const configLookup = new Map(relatedConfigs.map((c) => [c.id, c])); - const incoming = relationships.filter((r) => r.direction === 'incoming'); - const outgoing = relationships.filter((r) => r.direction === 'outgoing'); + const rels = relationships || []; + const incoming = rels.filter((r) => r.direction === 'incoming'); + const outgoing = rels.filter((r) => r.direction === 'outgoing'); return (
@@ -70,15 +72,8 @@ export default function ConfigRelationshipGraph({ centralConfig, relationships, ({centralConfig.status}) )}
- - {relationships.length === 0 ? ( -

No relationships found.

- ) : ( - <> - - - - )} + + ); } diff --git a/report/components/ConfigTreeSection.tsx b/report/components/ConfigTreeSection.tsx new file mode 100644 index 000000000..6555c61d9 --- /dev/null +++ b/report/components/ConfigTreeSection.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Section } from '@flanksource/facet'; +import { Icon } from '@flanksource/icons/icon'; +import type { CatalogReportTreeNode } from '../catalog-report-types.ts'; + +const EDGE_STYLES: Record = { + parent: { border: 'border-l-blue-300' }, + child: { border: 'border-l-green-300' }, + related: { border: 'border-l-purple-300' }, + target: { border: 'border-l-blue-500' }, +}; + +function TreeNodeRow({ node, depth = 0 }: { node: CatalogReportTreeNode; depth?: number }) { + const style = EDGE_STYLES[node.edgeType || 'child'] || EDGE_STYLES.child; + const isTarget = node.edgeType === 'target'; + const children = node.children || []; + + return ( +
+
+ {node.type && } + + {node.name} + + {node.type && ( + ({node.type}) + )} + {node.relation && ( + {node.relation} + )} +
+ {children.map((child, idx) => ( + + ))} +
+ ); +} + +interface Props { + tree: CatalogReportTreeNode; +} + +export default function ConfigTreeSection({ tree }: Props) { + if (!tree || !(tree.children || []).length) return null; + + return ( +
+ +
+ ); +} diff --git a/report/components/PageFooter.tsx b/report/components/PageFooter.tsx new file mode 100644 index 000000000..9a66e8b75 --- /dev/null +++ b/report/components/PageFooter.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { formatDateTime } from './utils.ts'; + +interface PageFooterProps { + publicURL?: string; + generatedAt?: string; + children?: React.ReactNode; +} + +export default function PageFooter({ publicURL, generatedAt, children }: PageFooterProps) { + const date = generatedAt ? formatDateTime(generatedAt) : formatDateTime(new Date().toISOString()); + + return ( +
+ {children} +
+ Generated {date} + {publicURL && ( + {publicURL} + )} +
+
+ ); +} diff --git a/report/components/PageHeader.tsx b/report/components/PageHeader.tsx new file mode 100644 index 000000000..9803e4035 --- /dev/null +++ b/report/components/PageHeader.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; + +interface PageHeaderProps { + subtitle?: string; +} + +export default function PageHeader({ subtitle }: PageHeaderProps) { + return ( +
+ + {subtitle && {subtitle}} +
+ ); +} diff --git a/report/components/RBACChangelogSection.tsx b/report/components/RBACChangelogSection.tsx index 0327cb6cf..604c42c03 100644 --- a/report/components/RBACChangelogSection.tsx +++ b/report/components/RBACChangelogSection.tsx @@ -22,11 +22,11 @@ function ChangeTypeBadge({ type }: { type: string }) { } interface Props { - changelog: RBACChangeEntry[]; + changelog?: RBACChangeEntry[]; } export default function RBACChangelogSection({ changelog }: Props) { - if (changelog.length === 0) return null; + if (!changelog?.length) return null; return (
diff --git a/report/components/RBACMatrixSection.tsx b/report/components/RBACMatrixSection.tsx index 1f60bb1fd..f6f49fa81 100644 --- a/report/components/RBACMatrixSection.tsx +++ b/report/components/RBACMatrixSection.tsx @@ -19,7 +19,7 @@ function buildMatrix(resource: RBACResource) { const roleSet = new Set(); const userMap = new Map(); - for (const u of resource.users) { + for (const u of resource.users || []) { roleSet.add(u.role); let row = userMap.get(u.userId); if (!row) { @@ -111,7 +111,7 @@ export default function RBACMatrixSection({ resource }: Props) { }; }); - const tags = { ...resource.tags, ...resource.labels }; + const tags = { ...(resource.tags || {}), ...(resource.labels || {}) }; const pathParts = resource.path?.split('.').filter(Boolean) ?? []; const corner = (
diff --git a/report/components/RBACSummarySection.tsx b/report/components/RBACSummarySection.tsx index a10168be1..0d5a149cd 100644 --- a/report/components/RBACSummarySection.tsx +++ b/report/components/RBACSummarySection.tsx @@ -17,12 +17,12 @@ export default function RBACSummarySection({ summary }: Props) { 0 ? 'warning' : 'default'} + color={summary.staleAccessCount > 0 ? 'orange' : undefined} /> 0 ? 'warning' : 'default'} + color={summary.overdueReviews > 0 ? 'red' : undefined} />
diff --git a/report/components/RBACUserSection.tsx b/report/components/RBACUserSection.tsx index 15e1b2489..2c04c8a66 100644 --- a/report/components/RBACUserSection.tsx +++ b/report/components/RBACUserSection.tsx @@ -72,7 +72,7 @@ function groupByConfigType(resources: RBACUserResource[]): Map @@ -100,7 +100,7 @@ export default function RBACUserSection({ user }: Props) { Resources: - {user.resources.length} + {(user.resources || []).length}
{[...grouped.entries()].map(([configType, resources]) => { diff --git a/report/components/rbac-visual.tsx b/report/components/rbac-visual.tsx index be45f21f3..b5228ccc6 100644 --- a/report/components/rbac-visual.tsx +++ b/report/components/rbac-visual.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LuUser, LuUsers, LuServer, LuBot } from 'react-icons/lu'; +import { Icon } from '@flanksource/icons/icon'; // --- Identity Types --- @@ -14,11 +14,11 @@ export interface IdentityInfo { const IDENTITY_COLOR = '#64748B'; -const IDENTITY_ICONS: Record> = { - user: LuUser, - group: LuUsers, - service: LuServer, - bot: LuBot, +const IDENTITY_ICON_NAMES: Record = { + user: 'user', + group: 'group', + service: 'server', + bot: 'bot', }; const IDENTITY_LABELS: Record = { @@ -103,8 +103,7 @@ export function ReviewOverdueLegendSwatch() { export function IdentityIcon({ userId, roleSource, size = 14 }: { userId: string; roleSource?: string; size?: number }) { const info = identityType(userId, roleSource); - const IconComponent = IDENTITY_ICONS[info.type]; - return ; + return ; } export function AccessIndicator({ direct, color, size = 2.5 }: { direct: boolean; color: string; size?: number }) { diff --git a/report/components/utils.ts b/report/components/utils.ts index 1a3fe8280..cd3d51aca 100644 --- a/report/components/utils.ts +++ b/report/components/utils.ts @@ -12,12 +12,60 @@ export function formatDateTime(iso: string): string { } export function formatRelative(iso: string): string { - const diff = Date.now() - new Date(iso).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 60) return `${mins}m ago`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; + return formatMonthDay(iso); +} + +export function formatTime(iso: string): string { + return new Date(iso).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); +} + +export function formatMonthDay(iso: string): string { + const d = new Date(iso); + const now = new Date(); + if (d.getFullYear() !== now.getFullYear()) { + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + } + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +} + +export type TimeBucketFormat = 'time' | 'monthDay'; + +export interface TimeBucket { + key: string; + label: string; + dateFormat: TimeBucketFormat; +} + +export function getTimeBucket(iso: string): TimeBucket { + const d = new Date(iso); + const now = new Date(); + const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const diffDays = Math.floor((startOfToday.getTime() - new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()) / 86400000); + + if (diffDays <= 0) { + return { key: 'today', label: formatDayLabel(d), dateFormat: 'time' }; + } + if (diffDays <= 6) { + return { key: `day-${diffDays}`, label: formatDayLabel(d), dateFormat: 'time' }; + } + if (diffDays <= 30) { + const weekStart = new Date(d); + weekStart.setDate(d.getDate() - d.getDay() + 1); + const weekEnd = new Date(weekStart); + weekEnd.setDate(weekStart.getDate() + 4); + const fmt = (dt: Date) => dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + return { key: `week-${fmt(weekStart)}`, label: `${fmt(weekStart)} – ${fmt(weekEnd)}`, dateFormat: 'monthDay' }; + } + const monthLabel = d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + return { key: `month-${d.getFullYear()}-${d.getMonth()}`, label: monthLabel, dateFormat: 'monthDay' }; +} + +function formatDayLabel(d: Date): string { + return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }); +} + +export function formatEntryDate(iso: string, fmt: TimeBucketFormat): string { + return fmt === 'time' ? formatTime(iso) : formatMonthDay(iso); } export function formatBytes(bytes: number): string { diff --git a/report/config-types.ts b/report/config-types.ts index f29bdfa71..80619723f 100644 --- a/report/config-types.ts +++ b/report/config-types.ts @@ -12,6 +12,7 @@ export interface ConfigItem { status?: string; health?: ConfigHealth; description?: string; + permalink?: string; labels?: Record; tags?: Record; costTotal30d?: number; @@ -19,9 +20,20 @@ export interface ConfigItem { updatedAt?: string; } +export interface ConfigChangeArtifact { + id: string; + filename: string; + contentType: string; + size: number; + dataUri?: string; +} + export interface ConfigChange { id?: string; configID?: string; + configName?: string; + configType?: string; + permalink?: string; changeType: string; severity?: ConfigSeverity; source?: string; @@ -31,11 +43,15 @@ export interface ConfigChange { createdAt?: string; firstObserved?: string; count?: number; + artifacts?: ConfigChangeArtifact[]; } export interface ConfigAnalysis { id?: string; configID?: string; + configName?: string; + configType?: string; + permalink?: string; analyzer: string; message?: string; summary?: string; diff --git a/report/embed.go b/report/embed.go index f0a6f64df..1d10ae75f 100644 --- a/report/embed.go +++ b/report/embed.go @@ -1,12 +1,34 @@ // Package report exposes the embedded TSX source files for the facet renderer. package report -import "embed" +import ( + "embed" + "os" + "path/filepath" +) //go:embed *.tsx *.ts package.json tsconfig.json components var FS embed.FS -// SourceDir overrides the embedded report files with a local directory. -// When set, facet renders use this directory directly instead of extracting -// embedded files to a cache directory. +// SourceDir overrides the embedded report files with a local directory or file. +// When set to a directory, facet renders use it directly instead of extracting +// embedded files. When set to a file, the file's directory is used and the +// filename overrides the entry file. var SourceDir string + +// ResolveSource returns the source directory and entry file override. +// If SourceDir points to a file, returns (dir, basename). +// If SourceDir points to a directory or is empty, returns (SourceDir, ""). +func ResolveSource() (dir string, entryFile string) { + if SourceDir == "" { + return "", "" + } + info, err := os.Stat(SourceDir) + if err != nil { + return SourceDir, "" + } + if !info.IsDir() { + return filepath.Dir(SourceDir), filepath.Base(SourceDir) + } + return SourceDir, "" +} diff --git a/report/facet.go b/report/facet.go index da62dd6f0..c67636be7 100644 --- a/report/facet.go +++ b/report/facet.go @@ -29,6 +29,9 @@ func RenderCLI(data any, format, entryFile string) ([]byte, error) { if err != nil { return nil, fmt.Errorf("prepare facet src dir: %w", err) } + if _, override := ResolveSource(); override != "" { + entryFile = override + } dataJSON, err := json.MarshalIndent(data, "", " ") if err != nil { @@ -152,8 +155,8 @@ func RenderHTTP(ctx context.Context, baseURL, token string, data any, format, en // SrcDir returns a stable directory containing the embedded report TSX files. // On first call it extracts the files; subsequent calls reuse the directory. var SrcDir = sync.OnceValues(func() (string, error) { - if SourceDir != "" { - return SourceDir, nil + if dir, _ := ResolveSource(); dir != "" { + return dir, nil } cacheDir, err := os.UserCacheDir() diff --git a/report/view-types.ts b/report/view-types.ts index 52626ea04..460f4ebab 100644 --- a/report/view-types.ts +++ b/report/view-types.ts @@ -101,9 +101,12 @@ export interface PanelResult { gauge?: GaugeConfig & { unit?: string }; bargauge?: BarGaugeConfig; timeseries?: TimeseriesConfig; + heatmap?: { mode?: string }; rows: Record[]; } +export type HeatmapVariant = 'calendar' | 'compact'; + export interface ViewSectionResult { title: string; icon?: string; From 4557a9c4ad053afbafecbb068d26c4211fdbe51e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 7 Apr 2026 06:46:45 +0300 Subject: [PATCH 30/48] refactor(api): clean up imports and simplify artifact handling Remove unused imports, consolidate artifact download to use blob storage abstraction, add config_change artifact listing endpoint, switch check/playbook conditional to case statement, and update query builder calls with BaseCatalogSearch wrapper. --- artifacts/controllers.go | 58 ++++++++++++++++++---------------------- auth/kratos.go | 2 -- mcp/access.go | 2 +- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/artifacts/controllers.go b/artifacts/controllers.go index bf29057b5..aaf876102 100644 --- a/artifacts/controllers.go +++ b/artifacts/controllers.go @@ -6,10 +6,8 @@ import ( "strings" "time" - "github.com/flanksource/artifacts" "github.com/flanksource/commons/logger" "github.com/flanksource/duty/api" - pkgConnection "github.com/flanksource/duty/connection" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" "github.com/flanksource/duty/query" @@ -17,7 +15,6 @@ import ( "github.com/labstack/echo/v4" "github.com/flanksource/duty/rbac/policy" - "github.com/flanksource/incident-commander/db" echoSrv "github.com/flanksource/incident-commander/echo" "github.com/flanksource/incident-commander/rbac" ) @@ -32,6 +29,7 @@ func RegisterRoutes(e *echo.Echo) { g := e.Group(fmt.Sprintf("/%s", "artifacts"), rbac.Authorization(policy.ObjectArtifact, policy.ActionRead)) g.GET("/list/check/:id/:check_time", ListArtifacts) g.GET("/list/playbook_run/:id", ListArtifacts) + g.GET("/list/config_change/:id", ListArtifacts) g.GET("/download/:id", DownloadArtifact) } @@ -45,19 +43,24 @@ func ListArtifacts(c echo.Context) error { return api.WriteError(c, api.Errorf(api.EINVALID, "invalid id(%s). must be a uuid. %v", _id, err)) } - _checkTime := c.Param("check_time") - checkTime, err := time.Parse(time.RFC3339, _checkTime) - if err != nil { - return api.WriteError(c, api.Errorf(api.EINVALID, "invalid check_time(%s). must be in RFC3339", _checkTime)) - } - var artifacts []models.Artifact - if strings.Contains(c.Path(), "/list/check/") { + switch { + case strings.Contains(c.Path(), "/list/check/"): + _checkTime := c.Param("check_time") + checkTime, err := time.Parse(time.RFC3339, _checkTime) + if err != nil { + return api.WriteError(c, api.Errorf(api.EINVALID, "invalid check_time(%s). must be in RFC3339", _checkTime)) + } artifacts, err = query.ArtifactsByCheck(ctx, id, checkTime) if err != nil { return api.WriteError(c, err) } - } else { + case strings.Contains(c.Path(), "/list/config_change/"): + artifacts, err = query.ArtifactsByConfigChange(ctx, id) + if err != nil { + return api.WriteError(c, err) + } + default: artifacts, err = query.ArtifactsByPlaybookRun(ctx, id) if err != nil { return api.WriteError(c, err) @@ -70,38 +73,29 @@ func ListArtifacts(c echo.Context) error { func DownloadArtifact(c echo.Context) error { ctx := c.Request().Context().(context.Context) - _id := c.Param("id") - artifactID, err := uuid.Parse(_id) + artifactID, err := uuid.Parse(c.Param("id")) if err != nil { - return api.WriteError(c, api.Errorf(api.EINVALID, "invalid id(%s). must be a uuid. %v", _id, err)) + return api.WriteError(c, api.Errorf(api.EINVALID, "invalid id: %v", err)) } - artifact, err := db.FindArtifact(ctx, artifactID) + blobs, err := ctx.Blobs() if err != nil { return api.WriteError(c, err) - } else if artifact == nil { - return api.WriteError(c, api.Errorf(api.ENOTFOUND, "artifact(%s) was not found", artifactID)) } + defer blobs.Close() - conn, err := pkgConnection.Get(ctx, artifact.ConnectionID.String()) + data, err := blobs.Read(artifactID) if err != nil { return api.WriteError(c, err) - } else if conn == nil { - return api.WriteError(c, api.Errorf(api.ENOTFOUND, "artifact's connection was not found")) } + defer data.Content.Close() - // TODO: Pool connection to the underlying filesystem - fs, err := artifacts.GetFSForConnection(ctx, *conn) - if err != nil { - return api.WriteError(c, err) + c.Response().Header().Set("Content-Type", data.ContentType) + if data.ContentLength > 0 { + c.Response().Header().Set("Content-Length", fmt.Sprintf("%d", data.ContentLength)) } - defer fs.Close() - - file, err := fs.Read(ctx, artifact.Path) - if err != nil { - return api.WriteError(c, err) + if filename := c.QueryParam("filename"); filename != "" { + c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) } - defer file.Close() - - return c.Stream(http.StatusOK, artifact.ContentType, file) + return c.Stream(http.StatusOK, data.ContentType, data.Content) } diff --git a/auth/kratos.go b/auth/kratos.go index b5ec90007..c2d3972f9 100644 --- a/auth/kratos.go +++ b/auth/kratos.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "time" @@ -13,7 +12,6 @@ import ( "github.com/flanksource/commons/rand" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" - incAPI "github.com/flanksource/incident-commander/api" "github.com/flanksource/incident-commander/db" "github.com/google/uuid" "github.com/labstack/echo/v4" diff --git a/mcp/access.go b/mcp/access.go index b496bd0a3..7fd3a3d0c 100644 --- a/mcp/access.go +++ b/mcp/access.go @@ -37,7 +37,7 @@ func searchCatalogAccessMappingHandler(goctx gocontext.Context, req mcp.CallTool var rows []db.RBACAccessRow err = auth.WithRLS(ctx, func(rlsCtx context.Context) error { - rows, err = db.GetRBACAccess(rlsCtx, []types.ResourceSelector{{Search: q}}) + rows, err = db.GetRBACAccess(rlsCtx, []types.ResourceSelector{{Search: q}}, false) return err }) if err != nil { From 7ecce472cb8b223b7cc0dc3bf76195505cdc413a Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 7 Apr 2026 10:28:55 +0300 Subject: [PATCH 31/48] refactor(ui): extract kitchen sink pages into modular components Split monolithic KitchenSink.tsx into specialized page modules (Layout, Config, Changes, Dynamic, Insights, Application, RBAC, Catalog, View) with shared type definitions. Introduces smart change section routing that auto-selects RBAC/backup/deployment renderers based on title and change patterns. --- report/KitchenSink.tsx | 299 +---- report/components/BackupActivityCalendar.tsx | 144 ++ report/components/BackupChanges.tsx | 200 +++ report/components/BackupsSection.tsx | 89 +- report/components/DeploymentChanges.tsx | 100 ++ report/components/DynamicSection.tsx | 61 +- report/components/RBACChanges.tsx | 126 ++ report/components/change-section-utils.ts | 366 ++++++ report/kitchen-sink-data.ts | 4 +- report/kitchen-sink/ApplicationPage.tsx | 36 + report/kitchen-sink/CatalogPage.tsx | 37 + report/kitchen-sink/ChangesPage.tsx | 45 + report/kitchen-sink/ConfigComponentsPage.tsx | 63 + report/kitchen-sink/DynamicSectionsPage.tsx | 60 + report/kitchen-sink/InsightsAndGraphPage.tsx | 57 + report/kitchen-sink/KitchenSinkTypes.ts | 21 + report/kitchen-sink/LayoutComponentsPage.tsx | 80 ++ report/kitchen-sink/RBACPage.tsx | 51 + report/kitchen-sink/ViewPage.tsx | 27 + report/testdata/kitchen-sink.yaml | 1227 +++++++++++++++++- report/types.ts | 10 + 21 files changed, 2757 insertions(+), 346 deletions(-) create mode 100644 report/components/BackupActivityCalendar.tsx create mode 100644 report/components/BackupChanges.tsx create mode 100644 report/components/DeploymentChanges.tsx create mode 100644 report/components/RBACChanges.tsx create mode 100644 report/components/change-section-utils.ts create mode 100644 report/kitchen-sink/ApplicationPage.tsx create mode 100644 report/kitchen-sink/CatalogPage.tsx create mode 100644 report/kitchen-sink/ChangesPage.tsx create mode 100644 report/kitchen-sink/ConfigComponentsPage.tsx create mode 100644 report/kitchen-sink/DynamicSectionsPage.tsx create mode 100644 report/kitchen-sink/InsightsAndGraphPage.tsx create mode 100644 report/kitchen-sink/KitchenSinkTypes.ts create mode 100644 report/kitchen-sink/LayoutComponentsPage.tsx create mode 100644 report/kitchen-sink/RBACPage.tsx create mode 100644 report/kitchen-sink/ViewPage.tsx diff --git a/report/KitchenSink.tsx b/report/KitchenSink.tsx index 858d83591..4afcd3ace 100644 --- a/report/KitchenSink.tsx +++ b/report/KitchenSink.tsx @@ -1,122 +1,27 @@ import React from 'react'; -import { Page, PageBreak, Section } from '@flanksource/facet'; -import type { ConfigReportData } from './config-types.ts'; -import ConfigLink from './components/ConfigLink.tsx'; -import ConfigChangesSection from './components/ConfigChangesSection.tsx'; -import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; -import ConfigRelationshipGraph from './components/ConfigRelationshipGraph.tsx'; -import ConfigItemCard from './components/ConfigItemCard.tsx'; -import ScraperCard from './components/ScraperCard.tsx'; -import { MatrixTable, Dot } from '@flanksource/facet'; -import type { ScraperInfo } from './scraper-types.ts'; - -const defaultData: ConfigReportData = { - configItem: { - id: 'cfg-eks-001', name: 'prod-eks-cluster', type: 'AWS::EKS::Cluster', - configClass: 'Cluster', status: 'Active', health: 'healthy', - description: 'Production EKS cluster running Mission Control workloads in us-east-1', - labels: { env: 'production', team: 'platform', region: 'us-east-1' }, - costTotal30d: 4280.50, createdAt: '2025-03-15T09:00:00Z', updatedAt: '2026-03-28T12:00:00Z', - }, - changes: [ - { id: 'chg-001', configID: 'cfg-eks-001', changeType: 'diff', severity: 'info', source: 'kubernetes', summary: 'Node pool autoscaler adjusted desired count from 3 to 5', createdBy: 'cluster-autoscaler', createdAt: '2026-03-30T08:15:00Z', count: 1 }, - { id: 'chg-002', configID: 'cfg-eks-001', changeType: 'Pulled', severity: 'info', source: 'kubernetes', summary: 'Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42', createdAt: '2026-03-30T07:30:00Z', count: 3 }, - { id: 'chg-003', configID: 'cfg-eks-001', changeType: 'ScalingReplicaSet', severity: 'low', source: 'kubernetes', summary: 'Deployment incident-commander scaled from 2 to 3 replicas', externalCreatedBy: 'hpa-controller', createdAt: '2026-03-29T22:00:00Z' }, - { id: 'chg-004', configID: 'cfg-eks-001', changeType: 'diff', severity: 'medium', source: 'terraform', summary: 'EKS cluster version upgraded from 1.28 to 1.29', createdBy: 'alice@flanksource.com', createdAt: '2026-03-29T14:00:00Z' }, - { id: 'chg-005', configID: 'cfg-eks-001', changeType: 'PolicyUpdate', severity: 'high', source: 'argocd', summary: 'Network policy updated: restricted egress to 10.0.0.0/8 for namespace mc', createdBy: 'bob@flanksource.com', createdAt: '2026-03-28T16:00:00Z' }, - { id: 'chg-006', configID: 'cfg-eks-001', changeType: 'diff', severity: 'critical', source: 'aws-config', summary: 'IAM role policy detached: eks-admin-access removed from cluster role', createdBy: 'security-automation', createdAt: '2026-03-28T10:00:00Z' }, - { id: 'chg-007', configID: 'cfg-eks-001', changeType: 'FieldsV1', severity: 'info', source: 'kubernetes', summary: 'ConfigMap kube-proxy updated with new CIDR ranges', createdAt: '2026-03-27T18:00:00Z', count: 2 }, - { id: 'chg-008', configID: 'cfg-eks-001', changeType: 'diff', severity: 'low', source: 'terraform', summary: 'Added tag cost-center=platform-engineering to cluster', createdBy: 'carol@flanksource.com', createdAt: '2026-03-27T09:00:00Z' }, - ], - analyses: [ - { id: 'ana-001', configID: 'cfg-eks-001', analyzer: 'Trivy', message: 'Container image flanksource/incident-commander:v1.4.200 has 3 high CVEs', status: 'open', severity: 'high', analysisType: 'security', source: 'trivy-operator', firstObserved: '2026-03-28T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - { id: 'ana-002', configID: 'cfg-eks-001', analyzer: 'Trivy', message: 'Base image golang:1.23-alpine has known vulnerability in libcrypto (CVE-2026-0891)', status: 'open', severity: 'critical', analysisType: 'security', source: 'trivy-operator', firstObserved: '2026-03-25T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - { id: 'ana-003', configID: 'cfg-eks-001', analyzer: 'OPA/Gatekeeper', message: 'Pod incident-commander-7f8b9c running as root user in namespace mc', status: 'open', severity: 'medium', analysisType: 'compliance', source: 'gatekeeper', firstObserved: '2026-03-20T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - { id: 'ana-004', configID: 'cfg-eks-001', analyzer: 'OPA/Gatekeeper', message: 'Namespace mc missing required label: data-classification', status: 'silenced', severity: 'low', analysisType: 'compliance', source: 'gatekeeper', firstObserved: '2026-03-15T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - { id: 'ana-005', configID: 'cfg-eks-001', analyzer: 'AWS Cost Optimizer', message: 'EKS node group i3.xlarge instances are underutilized (avg CPU 18%)', status: 'open', severity: 'medium', analysisType: 'cost', source: 'aws-cost-explorer', firstObserved: '2026-03-01T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - { id: 'ana-007', configID: 'cfg-eks-001', analyzer: 'Prometheus Advisor', message: 'P99 API response latency exceeded 500ms threshold 12 times in the last 7 days', status: 'open', severity: 'high', analysisType: 'performance', source: 'prometheus', firstObserved: '2026-03-23T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - { id: 'ana-010', configID: 'cfg-eks-001', analyzer: 'Prometheus Advisor', message: 'Node ip-10-0-2-18 memory utilization consistently above 85%', status: 'open', severity: 'high', analysisType: 'reliability', source: 'prometheus', firstObserved: '2026-03-26T09:00:00Z', lastObserved: '2026-03-30T09:00:00Z' }, - ], - relationships: [ - { configID: 'cfg-eks-001', relatedID: 'cfg-vpc-001', relation: 'RunsIn', direction: 'outgoing' }, - { configID: 'cfg-eks-001', relatedID: 'cfg-iam-001', relation: 'ManagedBy', direction: 'outgoing' }, - { configID: 'cfg-eks-001', relatedID: 'cfg-sg-001', relation: 'DependsOn', direction: 'outgoing' }, - { configID: 'cfg-eks-001', relatedID: 'cfg-rds-001', relation: 'DependsOn', direction: 'outgoing' }, - { configID: 'cfg-deploy-001', relatedID: 'cfg-eks-001', relation: 'RunsOn', direction: 'incoming' }, - { configID: 'cfg-deploy-002', relatedID: 'cfg-eks-001', relation: 'RunsOn', direction: 'incoming' }, - { configID: 'cfg-deploy-003', relatedID: 'cfg-eks-001', relation: 'RunsOn', direction: 'incoming' }, - { configID: 'cfg-ns-001', relatedID: 'cfg-eks-001', relation: 'ChildOf', direction: 'incoming' }, - { configID: 'cfg-node-001', relatedID: 'cfg-eks-001', relation: 'ChildOf', direction: 'incoming' }, - { configID: 'cfg-node-002', relatedID: 'cfg-eks-001', relation: 'ChildOf', direction: 'incoming' }, - ], - relatedConfigs: [ - { id: 'cfg-vpc-001', name: 'prod-vpc', type: 'AWS::EC2::VPC', configClass: 'Network', status: 'available', health: 'healthy', labels: { env: 'production' } }, - { id: 'cfg-iam-001', name: 'eks-cluster-role', type: 'AWS::IAM::Role', configClass: 'IAM', status: 'active', health: 'healthy' }, - { id: 'cfg-sg-001', name: 'eks-cluster-sg', type: 'AWS::EC2::SecurityGroup', configClass: 'Network', status: 'active', health: 'warning', labels: { env: 'production' } }, - { id: 'cfg-rds-001', name: 'mission-control-db', type: 'AWS::RDS::Instance', configClass: 'Database', status: 'available', health: 'healthy', labels: { env: 'production', engine: 'postgresql' } }, - { id: 'cfg-deploy-001', name: 'incident-commander', type: 'Kubernetes::Deployment', configClass: 'Deployment', status: 'Running', health: 'healthy', labels: { app: 'incident-commander' } }, - { id: 'cfg-deploy-002', name: 'canary-checker', type: 'Kubernetes::Deployment', configClass: 'Deployment', status: 'Running', health: 'healthy', labels: { app: 'canary-checker' } }, - { id: 'cfg-deploy-003', name: 'config-db', type: 'Kubernetes::Deployment', configClass: 'Deployment', status: 'Running', health: 'unhealthy', labels: { app: 'config-db' } }, - { id: 'cfg-ns-001', name: 'mc', type: 'Kubernetes::Namespace', configClass: 'Namespace', status: 'Active', health: 'healthy' }, - { id: 'cfg-node-001', name: 'ip-10-0-1-42', type: 'Kubernetes::Node', configClass: 'Node', status: 'Ready', health: 'healthy' }, - { id: 'cfg-node-002', name: 'ip-10-0-2-18', type: 'Kubernetes::Node', configClass: 'Node', status: 'Ready', health: 'warning', labels: { 'instance-type': 'i3.xlarge' } }, - ], -}; - -function PageHeader() { - return ( -
- Config Components - Kitchen Sink -
- ); -} - -function PageFooter() { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); - return ( -
- Generated {date} -
- ); -} - -function CoverContent() { - const date = new Date().toLocaleDateString('en-US', { - year: 'numeric', month: 'long', day: 'numeric', - }); - return ( -
-
-
- Component Showcase -
-

- Config Components -

-
- Kitchen Sink -
-

- PDF-compatible components for rendering config items, changes, insights, and relationships. -

-
-
-
Generated on {date}
-
- ); -} +import { Page } from '@flanksource/facet'; +import type { KitchenSinkData } from './kitchen-sink/KitchenSinkTypes.ts'; +import CoverPage from './components/CoverPage.tsx'; +import PageHeaderComponent from './components/PageHeader.tsx'; +import PageFooterComponent from './components/PageFooter.tsx'; +import LayoutComponentsPage from './kitchen-sink/LayoutComponentsPage.tsx'; +import ConfigComponentsPage from './kitchen-sink/ConfigComponentsPage.tsx'; +import ChangesPage from './kitchen-sink/ChangesPage.tsx'; +import DynamicSectionsPage from './kitchen-sink/DynamicSectionsPage.tsx'; +import InsightsAndGraphPage from './kitchen-sink/InsightsAndGraphPage.tsx'; +import ApplicationPage from './kitchen-sink/ApplicationPage.tsx'; +import RBACPage from './kitchen-sink/RBACPage.tsx'; +import CatalogPage from './kitchen-sink/CatalogPage.tsx'; +import ViewPage from './kitchen-sink/ViewPage.tsx'; interface KitchenSinkProps { - data?: ConfigReportData; + data: KitchenSinkData; } -export default function KitchenSink({ data: externalData }: KitchenSinkProps) { - const data = externalData?.configItem ? externalData : defaultData; - const header = ; - const footer = ; +export default function KitchenSink({ data }: KitchenSinkProps) { + + const header = ; + const footer = ; const pageProps = { pageSize: 'a4' as const, margins: { top: 5, bottom: 5, left: 5, right: 5 }, @@ -126,154 +31,34 @@ export default function KitchenSink({ data: externalData }: KitchenSinkProps) { footerHeight: 10, }; - const sampleConfigs = [data.configItem, ...data.relatedConfigs.slice(0, 5)]; - - const sampleScrapers: ScraperInfo[] = [ - { - id: 'scr-001', name: 'mc/aws-production', namespace: 'mc', - source: 'KubernetesCRD', types: ['aws', 'kubernetes'], - specHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd', - createdBy: 'alice@flanksource.com', createdAt: '2025-06-10T09:00:00Z', updatedAt: '2026-03-28T14:30:00Z', - gitops: { - git: { url: 'https://github.com/flanksource/mission-control-demo', branch: 'main', file: 'clusters/prod/scrapers/aws.yaml', dir: 'clusters/prod/scrapers', link: 'https://github.com/flanksource/mission-control-demo/tree/main/clusters/prod/scrapers/aws.yaml' }, - kustomize: { path: 'clusters/prod/scrapers', file: 'clusters/prod/scrapers/kustomization.yaml' }, - }, - }, - { - id: 'scr-002', name: 'mc/azure-entra', namespace: 'mc', - source: 'KubernetesCRD', types: ['azure'], - specHash: 'ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33', - createdAt: '2025-09-01T10:00:00Z', updatedAt: '2026-03-30T08:00:00Z', - }, - { - id: 'scr-003', name: 'local-file-scraper', - source: 'ConfigFile', types: ['file', 'sql'], - specHash: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', - createdBy: 'bob@flanksource.com', createdAt: '2026-01-15T11:00:00Z', - }, - ]; - return ( <> - - - - - - -
-
-
- Renders a config item as Icon + Name with optional health indicator. -
-
- {sampleConfigs.map((config) => ( -
-
- -
-
- -
- {config.type} -
- ))} -
-
-
-
- - - - -
-
- Renders a config item with icon, name, tags, and metadata. -
-
- {sampleConfigs.map((config) => ( -
- -
- ))} -
-
-
- - - - -
-
- Renders a scraper with type icons, source badge, spec hash, created by, dates, and GitOps provenance. -
-
- {sampleScrapers.map((scraper) => ( - - ))} -
-
-
- - - - - + +

+ PDF-compatible components for rendering config items, changes, insights, and relationships. +

+
- - - - - - - - - - - - - - - -
-
- Rotated column headers using CSS-Tricks translate+rotate pattern. -
- alice@example.com, cells: [, , , , null, null] }, - { label: bob@example.com, cells: [, , null, null, null, null] }, - { label: charlie@example.com, cells: [, null, null, null, null, ] }, - { label: deploy-bot, cells: [, , , null, null, null] }, - { label: monitoring-svc, cells: [, null, null, null, null, ] }, - ]} - /> -
- With longer column names and more rows. -
- design-studio-pas, cells: [null, null, , null, null, null, null] }, - { label: monitoring_ro, cells: [, null, null, null, null, null, null] }, - { label: oipa-qa-bot, cells: [null, null, , null, null, null, null] }, - { label: omasa, cells: [null, null, , null, null, null, null] }, - { label: SG-OMAR Shared Dev DB, cells: [null, , null, null, , null, null] }, - { label: SG-OMAR Shared RO, cells: [, null, null, null, null, null, ] }, - { label: svc_mission_control, cells: [, , null, null, null, null, null] }, - ]} - /> -
-
+ + + + + + + + + ); } diff --git a/report/components/BackupActivityCalendar.tsx b/report/components/BackupActivityCalendar.tsx new file mode 100644 index 000000000..4df4dd2d6 --- /dev/null +++ b/report/components/BackupActivityCalendar.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import type { BackupCalendarEntry, BackupCalendarStatus } from './change-section-utils.ts'; + +interface Props { + entries: BackupCalendarEntry[]; +} + +const DAY_HEADERS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; + +const CELL_CLASSES: Record = { + success: 'bg-green-50 border border-green-600', + failed: 'bg-red-50 border border-red-600', + warning: 'bg-amber-50 border border-amber-500', + none: 'bg-gray-100 border border-gray-200', +}; + +const LABEL_CLASSES: Record = { + success: 'text-green-700', + failed: 'text-red-600', + warning: 'text-amber-700', +}; + +const STATUS_RANK: Record = { + success: 1, + warning: 2, + failed: 3, +}; + +interface AggregatedEntry { + date: string; + status: BackupCalendarStatus; + label?: string; + count: number; +} + +function aggregateEntries(entries: BackupCalendarEntry[]): AggregatedEntry[] { + const byDay = new Map(); + + for (const entry of entries) { + const key = entry.date.slice(0, 10); + const current = byDay.get(key); + + if (!current) { + byDay.set(key, { + date: entry.date, + status: entry.status, + label: entry.label, + count: 1, + }); + continue; + } + + current.count += 1; + if (new Date(entry.date).getTime() >= new Date(current.date).getTime()) { + current.date = entry.date; + current.label = entry.label ?? current.label; + } + if (STATUS_RANK[entry.status] >= STATUS_RANK[current.status]) { + current.status = entry.status; + } + } + + return [...byDay.values()]; +} + +export default function BackupActivityCalendar({ entries }: Props) { + if (!entries.length) { + return null; + } + + const aggregated = aggregateEntries(entries); + const referenceDate = new Date(aggregated.reduce((latest, entry) => ( + new Date(entry.date).getTime() > new Date(latest.date).getTime() ? entry : latest + )).date); + + const year = referenceDate.getFullYear(); + const month = referenceDate.getMonth(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const firstDow = new Date(year, month, 1).getDay(); + + const dateMap: Record = {}; + for (const entry of aggregated) { + dateMap[entry.date.slice(0, 10)] = entry; + } + + const monthLabel = referenceDate.toLocaleString('default', { month: 'long', year: 'numeric' }); + const cells: (number | null)[] = [ + ...Array(firstDow).fill(null), + ...Array.from({ length: daysInMonth }, (_, index) => index + 1), + ]; + + return ( +
+

{monthLabel}

+
+ {DAY_HEADERS.map((day) => ( +
{day}
+ ))} + {cells.map((day, index) => { + if (day === null) { + return
; + } + + const key = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const entry = dateMap[key]; + const cellClass = entry ? CELL_CLASSES[entry.status] : CELL_CLASSES.none; + const label = entry ? (entry.count > 1 ? `×${entry.count}` : entry.label ?? entry.status) : ''; + + return ( +
+ {day} + {entry && ( + + {label} + + )} +
+ ); + })} +
+
+ + + Success + + + + Failed + + + + In Progress + + + + No backup + +
+
+ ); +} diff --git a/report/components/BackupChanges.tsx b/report/components/BackupChanges.tsx new file mode 100644 index 000000000..909b5a4e1 --- /dev/null +++ b/report/components/BackupChanges.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { StatCard, ListTable } from '@flanksource/facet'; +import type { ApplicationChange } from '../types.ts'; +import { formatDateTime } from './utils.ts'; +import BackupActivityCalendar from './BackupActivityCalendar.tsx'; +import { + filterBackupChanges, + getBackupCalendarStatus, + getChangeActor, + isRestoreChange, + toBackupCalendarEntries, +} from './change-section-utils.ts'; + +interface Props { + changes: ApplicationChange[]; +} + +const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; +const TIMESTAMP_VALUE_CLASS = 'text-[8pt] leading-[10pt]'; +const BACKUP_TAG_MAPPING = (key: string, value: unknown): string => { + if (key !== 'state' && key !== 'type') { + return ''; + } + + const normalized = String(value).toLowerCase(); + if (normalized.includes('fail')) { + return 'text-red-700 bg-red-50 border-red-200'; + } + if (normalized.includes('running') || normalized.includes('progress') || normalized.includes('started') || normalized.includes('queued')) { + return 'text-orange-700 bg-orange-50 border-orange-200'; + } + if (normalized.includes('complete') || normalized.includes('success')) { + return 'text-green-700 bg-green-50 border-green-200'; + } + if (normalized.includes('restore')) { + return 'text-blue-700 bg-blue-50 border-blue-200'; + } + return 'text-gray-600 bg-gray-50 border-gray-200'; +}; + +function attentionLabel(change: ApplicationChange): string { + const status = getBackupCalendarStatus(change); + if (status === 'failed') { + return 'Failed'; + } + if (status === 'warning') { + return 'In Progress'; + } + return change.changeType ?? 'Backup'; +} + +export default function BackupChanges({ changes }: Props) { + const relevant = filterBackupChanges(changes); + if (!relevant.length) { + return null; + } + + const backupEvents = relevant.filter((change) => !isRestoreChange(change)); + const restoreEvents = relevant.filter(isRestoreChange); + const completed = backupEvents.filter((change) => getBackupCalendarStatus(change) === 'success'); + const failed = backupEvents.filter((change) => getBackupCalendarStatus(change) === 'failed'); + const inProgress = backupEvents.filter((change) => getBackupCalendarStatus(change) === 'warning'); + const latestSuccessful = completed.reduce((latest, change) => { + if (!latest) return change; + return new Date(change.date).getTime() > new Date(latest.date).getTime() ? change : latest; + }, null); + const latestSuccessfulValue = latestSuccessful ? formatDateTime(latestSuccessful.date) : 'None'; + const latestSuccessfulColor = latestSuccessful + ? 'green' + : backupEvents.length > 0 + ? 'red' + : 'gray'; + + return ( + <> +
0 ? 'grid-cols-4' : 'grid-cols-3'} gap-[3mm] mb-[4mm]`}> + 0 ? 'Needs attention' : 'No failures'} + variant="summary" + size="sm" + color={failed.length > 0 ? 'red' : 'gray'} + valueClassName={COUNT_VALUE_CLASS} + /> + 0 ? 'orange' : 'gray'} + valueClassName={COUNT_VALUE_CLASS} + /> + + {restoreEvents.length > 0 && ( + + )} +
+ + {backupEvents.length > 0 && ( +
+ +
+ )} + + {attentionRows.length > 0 && ( +
+

Exceptions & Running Jobs

+ ({ + date: change.date, + subject: change.description, + subtitle: `Changed by ${getChangeActor(change)}`, + state: attentionLabel(change), + sourceLabel: `Source: ${change.source || '-'}`, + }))} + subject="subject" + subtitle="subtitle" + date="date" + dateFormat="long" + primaryTags={['state']} + keys={['sourceLabel']} + tagMapping={BACKUP_TAG_MAPPING} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + /> +
+ )} + + {restoreEvents.length > 0 && ( +
+

Restore Jobs

+ ({ + date: change.date, + subject: change.description, + subtitle: `Changed by ${getChangeActor(change)}`, + type: change.changeType ?? 'Restore', + sourceLabel: `Source: ${change.source || '-'}`, + }))} + subject="subject" + subtitle="subtitle" + date="date" + dateFormat="long" + primaryTags={['type']} + keys={['sourceLabel']} + tagMapping={BACKUP_TAG_MAPPING} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + /> +
+ )} + +
+

Event Stream

+ ({ + date: change.date, + subject: change.description, + subtitle: `Changed by ${getChangeActor(change)}`, + type: change.changeType ?? 'Event', + sourceLabel: `Source: ${change.source || '-'}`, + }))} + subject="subject" + subtitle="subtitle" + date="date" + dateFormat="long" + primaryTags={['type']} + keys={['sourceLabel']} + tagMapping={BACKUP_TAG_MAPPING} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + /> +
+ + ); +} diff --git a/report/components/BackupsSection.tsx b/report/components/BackupsSection.tsx index 364ea273e..f7ea597e0 100644 --- a/report/components/BackupsSection.tsx +++ b/report/components/BackupsSection.tsx @@ -2,95 +2,22 @@ import React from 'react'; import { Section, StatCard, CompactTable } from '@flanksource/facet'; import type { ApplicationBackup, ApplicationBackupRestore } from '../types.ts'; import { formatDateTime } from './utils.ts'; +import BackupActivityCalendar from './BackupActivityCalendar.tsx'; +import type { BackupCalendarStatus } from './change-section-utils.ts'; interface Props { backups: ApplicationBackup[]; restores: ApplicationBackupRestore[]; } -const DAY_HEADERS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; - -const CELL_CLASSES: Record = { - success: 'bg-green-50 border border-green-600', - failed: 'bg-red-50 border border-red-600', - none: 'bg-gray-100 border border-gray-200', -}; - -const SIZE_TEXT_CLASSES: Record = { - success: 'text-green-700', - failed: 'text-red-600', -}; - -function BackupCalendar({ backups }: { backups: ApplicationBackup[] }) { - const referenceDate = backups.length > 0 - ? new Date(backups.reduce((a, b) => (a.date > b.date ? a : b)).date) - : new Date(); - - const year = referenceDate.getFullYear(); - const month = referenceDate.getMonth(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - const firstDow = new Date(year, month, 1).getDay(); - - const dateMap: Record = {}; - for (const b of backups) { - dateMap[b.date.slice(0, 10)] = b; - } - - const monthLabel = referenceDate.toLocaleString('default', { month: 'long', year: 'numeric' }); - const cells: (number | null)[] = [ - ...Array(firstDow).fill(null), - ...Array.from({ length: daysInMonth }, (_, i) => i + 1), - ]; - - return ( -
-

{monthLabel}

-
- {DAY_HEADERS.map((d) => ( -
{d}
- ))} - {cells.map((day, idx) => { - if (day === null) return
; - const key = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; - const backup = dateMap[key]; - const cellClass = backup ? CELL_CLASSES[backup.status] ?? CELL_CLASSES.none : CELL_CLASSES.none; - return ( -
- {day} - {backup && ( - - {backup.size} - - )} -
- ); - })} -
-
- - - Success - - - - Failed - - - - No backup - -
-
- ); -} - - export default function BackupsSection({ backups, restores }: Props) { const successCount = backups.filter((b) => b.status === 'success').length; const failedCount = backups.filter((b) => b.status !== 'success').length; + const calendarEntries = backups.map((backup) => ({ + date: backup.date, + status: (backup.status === 'success' ? 'success' : backup.status === 'failed' ? 'failed' : 'warning') as BackupCalendarStatus, + label: backup.size || undefined, + })); const failedRows = backups .filter((b) => b.status !== 'success') @@ -111,7 +38,7 @@ export default function BackupsSection({ backups, restores }: Props) {
- +
{failedRows.length > 0 && (
diff --git a/report/components/DeploymentChanges.tsx b/report/components/DeploymentChanges.tsx new file mode 100644 index 000000000..a3b7d3637 --- /dev/null +++ b/report/components/DeploymentChanges.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { StatCard } from '@flanksource/facet'; +import type { ApplicationChange } from '../types.ts'; +import { formatEntryDate, getTimeBucket, type TimeBucketFormat } from './utils.ts'; +import { + classifyDeploymentChange, + filterDeploymentChanges, + getChangeActor, +} from './change-section-utils.ts'; + +interface Props { + changes: ApplicationChange[]; +} + +const CATEGORY_STYLES: Record<'scale' | 'policy' | 'spec', string> = { + scale: 'bg-blue-50 text-blue-700 border-blue-200', + policy: 'bg-orange-50 text-orange-700 border-orange-200', + spec: 'bg-slate-50 text-slate-700 border-slate-200', +}; + +interface BucketGroup { + key: string; + label: string; + dateFormat: TimeBucketFormat; + changes: ApplicationChange[]; +} + +function groupByTimeBucket(changes: ApplicationChange[]): BucketGroup[] { + const groups: BucketGroup[] = []; + const groupMap = new Map(); + + for (const change of changes) { + const bucket = getTimeBucket(change.date); + let group = groupMap.get(bucket.key); + if (!group) { + group = { key: bucket.key, label: bucket.label, dateFormat: bucket.dateFormat, changes: [] }; + groupMap.set(bucket.key, group); + groups.push(group); + } + group.changes.push(change); + } + + return groups; +} + +export default function DeploymentChanges({ changes }: Props) { + const relevant = filterDeploymentChanges(changes).sort((a, b) => ( + new Date(b.date).getTime() - new Date(a.date).getTime() + )); + + if (!relevant.length) { + return null; + } + + const counts = { + scale: relevant.filter((change) => classifyDeploymentChange(change) === 'scale').length, + policy: relevant.filter((change) => classifyDeploymentChange(change) === 'policy').length, + spec: relevant.filter((change) => classifyDeploymentChange(change) === 'spec').length, + }; + + const groups = groupByTimeBucket(relevant); + + return ( + <> +
+ + + + +
+ + {groups.map((group) => ( +
+
+ {group.label} + ({group.changes.length}) +
+
+ {group.changes.map((change) => { + const category = classifyDeploymentChange(change) ?? 'spec'; + return ( +
+ + {formatEntryDate(change.date, group.dateFormat)} + + + {category} + + {change.changeType ?? '-'} + {getChangeActor(change)} + {change.description} +
+ ); + })} +
+
+ ))} + + ); +} diff --git a/report/components/DynamicSection.tsx b/report/components/DynamicSection.tsx index 665eda1fe..8ff685eb1 100644 --- a/report/components/DynamicSection.tsx +++ b/report/components/DynamicSection.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { Section, CompactTable } from '@flanksource/facet'; import type { ApplicationSection, ViewColumnType } from '../types.ts'; +import RBACChanges from './RBACChanges.tsx'; +import BackupChanges from './BackupChanges.tsx'; +import DeploymentChanges from './DeploymentChanges.tsx'; import { formatDate, formatRelative, @@ -10,6 +13,12 @@ import { SEVERITY_COLORS, SEVERITY_BG, } from './utils.ts'; +import { + filterBackupChanges, + filterDeploymentChanges, + filterRBACChanges, + inferChangeSectionVariant, +} from './change-section-utils.ts'; interface Props { section: ApplicationSection; @@ -129,7 +138,7 @@ function ViewSection({ section }: { section: ApplicationSection }) { ); } -function ChangesSection({ section }: { section: ApplicationSection }) { +function GenericChangesSection({ section }: { section: ApplicationSection }) { const rows = (section.changes ?? []).map((c) => [ formatRelative(c.date), c.changeType ?? '-', @@ -140,6 +149,22 @@ function ChangesSection({ section }: { section: ApplicationSection }) { return ; } +function ChangesSection({ section }: { section: ApplicationSection }) { + const changes = section.changes ?? []; + if (!changes.length) return null; + + switch (inferChangeSectionVariant(section.title, changes)) { + case 'rbac': + return ; + case 'backup': + return ; + case 'deployment': + return ; + default: + return ; + } +} + function ConfigsSection({ section }: { section: ApplicationSection }) { const rows = (section.configs ?? []).map((c) => [ c.name, @@ -152,11 +177,39 @@ function ConfigsSection({ section }: { section: ApplicationSection }) { } export default function DynamicSection({ section }: Props) { + if (section.type === 'changes') { + const changes = section.changes ?? []; + const variant = inferChangeSectionVariant(section.title, changes); + const renderable = variant === 'rbac' + ? filterRBACChanges(changes).length > 0 + : variant === 'backup' + ? filterBackupChanges(changes).length > 0 + : variant === 'deployment' + ? filterDeploymentChanges(changes).length > 0 + : changes.length > 0; + + if (!renderable) { + return null; + } + } + + let content: React.ReactNode = null; + + if (section.type === 'view') { + content = ; + } else if (section.type === 'changes') { + content = ; + } else if (section.type === 'configs') { + content = ; + } + + if (!content) { + return null; + } + return (
- {section.type === 'view' && } - {section.type === 'changes' && } - {section.type === 'configs' && } + {content}
); } diff --git a/report/components/RBACChanges.tsx b/report/components/RBACChanges.tsx new file mode 100644 index 000000000..a2c2da9e3 --- /dev/null +++ b/report/components/RBACChanges.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { StatCard } from '@flanksource/facet'; +import type { ApplicationChange } from '../types.ts'; +import ConfigLink from './ConfigLink.tsx'; +import { IdentityIcon } from './rbac-visual.tsx'; +import { formatDateTime } from './utils.ts'; +import { filterRBACChanges, groupRBACChanges, type RBACChangeRow } from './change-section-utils.ts'; + +interface Props { + changes: ApplicationChange[]; +} + +const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; +const ACTION_BADGE_CLASSES: Record<'Granted' | 'Revoked', string> = { + Granted: 'text-green-700 bg-green-50 border-green-200', + Revoked: 'text-red-700 bg-red-50 border-red-200', +}; + +function PermissionRow({ row }: { row: RBACChangeRow }) { + const changedBy = row.changedBy !== '-' ? row.changedBy : row.source; + const roleLabel = row.role || 'Access'; + const identityRoleSource = row.subjectKind === 'group' ? `group:${row.subject}` : undefined; + + return ( +
+
+
+ + {row.action} + + {roleLabel} + to + + + {row.subject} + + {row.viaGroup && ( + via {row.viaGroup} + )} + + {formatDateTime(row.date)} + +
+
+ Changed by {changedBy} +
+
+ {row.notes && ( +
+ {row.notes} +
+ )} +
+ ); +} + +export default function RBACChanges({ changes }: Props) { + const relevant = filterRBACChanges(changes); + if (!relevant.length) { + return null; + } + + const groups = groupRBACChanges(relevant); + const grantedCount = groups.reduce((total, group) => total + group.rows.filter((row) => row.action === 'Granted').length, 0); + const revokedCount = groups.reduce((total, group) => total + group.rows.filter((row) => row.action === 'Revoked').length, 0); + const netCount = grantedCount - revokedCount; + const netColor = netCount > 0 ? 'orange' : netCount < 0 ? 'green' : 'gray'; + + return ( + <> +
+ 0 ? 'orange' : 'gray'} + valueClassName={COUNT_VALUE_CLASS} + /> + 0 ? 'green' : 'gray'} + valueClassName={COUNT_VALUE_CLASS} + /> + 0 ? `+${netCount}` : String(netCount)} + sublabel="Granted minus revoked" + variant="summary" + size="sm" + color={netColor} + valueClassName={COUNT_VALUE_CLASS} + /> +
+
+ {groups.map((group) => ( +
+
+
+ +
+ {group.configType && ( + + {group.configType} + + )} + + {group.rows.length} change{group.rows.length === 1 ? '' : 's'} + +
+
+ {group.rows.map((row) => ( + + ))} +
+
+ ))} +
+ + ); +} diff --git a/report/components/change-section-utils.ts b/report/components/change-section-utils.ts new file mode 100644 index 000000000..42ed6c0ac --- /dev/null +++ b/report/components/change-section-utils.ts @@ -0,0 +1,366 @@ +import type { ApplicationChange, ApplicationPermissionChange } from '../types.ts'; + +export type ChangeSectionVariant = 'generic' | 'rbac' | 'backup' | 'deployment'; +export type BackupCalendarStatus = 'success' | 'failed' | 'warning'; +export type RBACChangeAction = 'added' | 'removed'; + +export interface BackupCalendarEntry { + date: string; + status: BackupCalendarStatus; + label?: string; +} + +export interface RBACChangeRow { + id: string; + date: string; + action: 'Granted' | 'Revoked'; + subject: string; + subjectKind: 'user' | 'group'; + role?: string; + viaGroup?: string; + changedBy: string; + source: string; + notes?: string; +} + +export interface RBACChangeGroup { + key: string; + configId?: string; + configName: string; + configType?: string; + latestDate: string; + rows: RBACChangeRow[]; +} + +const RBAC_ADDED_TYPES = new Set(['PermissionGranted', 'PermissionAdded']); +const RBAC_REMOVED_TYPES = new Set(['PermissionRevoked', 'PermissionRemoved']); +const BACKUP_SUCCESS_TYPES = new Set(['BackupCompleted', 'BackupSuccessful']); +const BACKUP_FAILED_TYPES = new Set(['BackupFailed']); +const BACKUP_PROGRESS_TYPES = new Set(['BackupStarted', 'BackupRunning', 'BackupEnqueued']); +const RESTORE_CHANGE_TYPES = new Set(['BackupRestored', 'RestoreCompleted']); +const DEPLOYMENT_CHANGE_TYPES = new Set(['ScalingReplicaSet', 'PolicyUpdate']); + +function normalizedType(change: ApplicationChange): string { + return change.changeType ?? ''; +} + +export function getChangeActor(change: ApplicationChange): string { + return change.createdBy || change.source || '-'; +} + +function normalizeRBACAction(change: ApplicationChange): RBACChangeAction | null { + const type = normalizedType(change); + if (RBAC_ADDED_TYPES.has(type)) { + return 'added'; + } + if (RBAC_REMOVED_TYPES.has(type)) { + return 'removed'; + } + return null; +} + +export function isRBACChange(change: ApplicationChange): boolean { + return normalizeRBACAction(change) !== null; +} + +export function isBackupChange(change: ApplicationChange): boolean { + const type = normalizedType(change); + return BACKUP_SUCCESS_TYPES.has(type) || BACKUP_FAILED_TYPES.has(type) || BACKUP_PROGRESS_TYPES.has(type) || RESTORE_CHANGE_TYPES.has(type); +} + +export function isRestoreChange(change: ApplicationChange): boolean { + return RESTORE_CHANGE_TYPES.has(normalizedType(change)); +} + +export function classifyDeploymentChange(change: ApplicationChange): 'scale' | 'policy' | 'spec' | null { + const type = normalizedType(change); + const description = change.description.toLowerCase(); + const lowerType = type.toLowerCase(); + + if (type === 'ScalingReplicaSet' || lowerType.includes('replicaset') || /scaled|scaling|replica/.test(description)) { + return 'scale'; + } + + if (type === 'PolicyUpdate' || description.includes('policy')) { + return 'policy'; + } + + if ( + DEPLOYMENT_CHANGE_TYPES.has(type) || + (type === 'diff' && ( + description.includes('deployment') || + description.includes('rollout') || + description.includes('image updated') + )) || + description.includes('deployment') || + description.includes('rollout') || + description.includes('image updated') + ) { + return 'spec'; + } + + return null; +} + +export function isDeploymentChange(change: ApplicationChange): boolean { + return classifyDeploymentChange(change) !== null; +} + +export function filterRBACChanges(changes: ApplicationChange[]): ApplicationChange[] { + return changes.filter(isRBACChange); +} + +export function filterBackupChanges(changes: ApplicationChange[]): ApplicationChange[] { + return changes.filter(isBackupChange); +} + +export function filterDeploymentChanges(changes: ApplicationChange[]): ApplicationChange[] { + return changes.filter(isDeploymentChange); +} + +export function inferChangeSectionVariant(title: string, changes: ApplicationChange[]): ChangeSectionVariant { + const lowerTitle = title.toLowerCase(); + + if (/\brbac\b|\bpermission/.test(lowerTitle)) { + return 'rbac'; + } + + if (/\bbackup\b|\brestore\b/.test(lowerTitle)) { + return 'backup'; + } + + if (/\bdeployment\b|\brollout\b/.test(lowerTitle)) { + return 'deployment'; + } + + const rbacCount = filterRBACChanges(changes).length; + const backupCount = filterBackupChanges(changes).length; + const deploymentCount = filterDeploymentChanges(changes).length; + + if (rbacCount > 0 && rbacCount === changes.length) { + return 'rbac'; + } + + if (backupCount > 0 && backupCount === changes.length) { + return 'backup'; + } + + if (deploymentCount > 0 && deploymentCount >= Math.ceil(changes.length / 2)) { + return 'deployment'; + } + + return 'generic'; +} + +export function getBackupCalendarStatus(change: ApplicationChange): BackupCalendarStatus | null { + const type = normalizedType(change); + if (BACKUP_SUCCESS_TYPES.has(type)) { + return 'success'; + } + if (BACKUP_FAILED_TYPES.has(type)) { + return 'failed'; + } + if (BACKUP_PROGRESS_TYPES.has(type)) { + return 'warning'; + } + return null; +} + +export function extractBackupLabel(change: ApplicationChange): string | undefined { + const match = change.description.match(/(\d+(?:\.\d+)?)\s*([KMGT]i?B|bytes?)/i); + if (!match) { + return undefined; + } + + return `${match[1]} ${match[2].toUpperCase()}`; +} + +export function toBackupCalendarEntries(changes: ApplicationChange[]): BackupCalendarEntry[] { + return filterBackupChanges(changes) + .filter((change) => !isRestoreChange(change)) + .map((change) => { + const status = getBackupCalendarStatus(change); + if (!status) { + return null; + } + + return { + date: change.date, + status, + label: extractBackupLabel(change), + }; + }) + .filter(Boolean) as BackupCalendarEntry[]; +} + +interface ParsedPermissionSummary { + user?: string; + role?: string; + group?: string; + resourceName?: string; +} + +function cleanField(value?: string): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +function parseStructuredPermissionSummary(description: string): ParsedPermissionSummary { + const match = description.match(/^Permission(?:Added|Removed):\s*(.+)$/i); + if (!match) { + return {}; + } + + const parsed: ParsedPermissionSummary = {}; + for (const part of match[1].split(/\s*,\s*/)) { + const userMatch = part.match(/^user\s+(.+)$/i); + if (userMatch) { + parsed.user = cleanField(userMatch[1]); + continue; + } + + const roleMatch = part.match(/^role\s+(.+)$/i); + if (roleMatch) { + parsed.role = cleanField(roleMatch[1]); + continue; + } + + const groupMatch = part.match(/^group\s+(.+)$/i); + if (groupMatch) { + parsed.group = cleanField(groupMatch[1]); + } + } + + return parsed; +} + +function parseLegacyPermissionSummary(description: string): ParsedPermissionSummary { + const grantedWithPermissions = description.match(/^Granted\s+(.+?)\s+permissions?\s+to\s+(.+?)\s+on\s+(.+)$/i); + if (grantedWithPermissions) { + return { + role: cleanField(grantedWithPermissions[1]), + user: cleanField(grantedWithPermissions[2]), + resourceName: cleanField(grantedWithPermissions[3]), + }; + } + + const granted = description.match(/^Granted\s+(.+?)\s+to\s+(.+?)\s+on\s+(.+)$/i); + if (granted) { + return { + role: cleanField(granted[1]), + user: cleanField(granted[2]), + resourceName: cleanField(granted[3]), + }; + } + + const revoked = description.match(/^Revoked\s+(.+?)\s+access\s+for\s+(.+?)\s+on\s+(.+)$/i); + if (revoked) { + return { + role: cleanField(revoked[1]), + user: cleanField(revoked[2]), + resourceName: cleanField(revoked[3]), + }; + } + + return {}; +} + +function parsePermissionSummary(description: string): ParsedPermissionSummary { + const structured = parseStructuredPermissionSummary(description); + if (structured.user || structured.role || structured.group) { + return structured; + } + return parseLegacyPermissionSummary(description); +} + +function isRedundantPermissionDescription(change: ApplicationChange, permission: ApplicationPermissionChange, parsed: ParsedPermissionSummary): boolean { + const description = change.description.trim(); + if (!description) { + return true; + } + + if (/^Permission(?:Added|Removed):/i.test(description)) { + return true; + } + + if (/^(Granted|Revoked)\b/i.test(description)) { + const matchedUser = cleanField(permission.user) || parsed.user; + const matchedRole = cleanField(permission.role) || parsed.role; + const matchedResource = cleanField(change.configName) || parsed.resourceName; + + if ( + (!matchedUser || description.includes(matchedUser)) && + (!matchedRole || description.includes(matchedRole)) && + (!matchedResource || description.includes(matchedResource)) + ) { + return true; + } + } + + return false; +} + +export function groupRBACChanges(changes: ApplicationChange[]): RBACChangeGroup[] { + const grouped = new Map(); + + for (const change of filterRBACChanges(changes)) { + const action = normalizeRBACAction(change); + if (!action) { + continue; + } + + const parsed = parsePermissionSummary(change.description); + const permission = change.permission ?? {}; + const configName = cleanField(change.configName) || parsed.resourceName || 'Unknown resource'; + const configId = cleanField(change.configId); + const key = configId || configName; + const explicitUser = cleanField(permission.user) || parsed.user; + const explicitGroup = cleanField(permission.group) || parsed.group; + const subject = explicitUser || explicitGroup || '-'; + const subjectKind = explicitUser ? 'user' : explicitGroup ? 'group' : 'user'; + const role = cleanField(permission.role) || parsed.role; + const viaGroup = explicitUser && explicitGroup ? explicitGroup : undefined; + const notes = isRedundantPermissionDescription(change, permission, parsed) + ? undefined + : cleanField(change.description); + + if (!grouped.has(key)) { + grouped.set(key, { + key, + configId, + configName, + configType: cleanField(change.configType), + latestDate: change.date, + rows: [], + }); + } + + const group = grouped.get(key)!; + if (!group.configType && change.configType) { + group.configType = change.configType; + } + if (new Date(change.date).getTime() > new Date(group.latestDate).getTime()) { + group.latestDate = change.date; + } + + group.rows.push({ + id: change.id, + date: change.date, + action: action === 'added' ? 'Granted' : 'Revoked', + subject, + subjectKind, + role, + viaGroup, + changedBy: cleanField(change.createdBy) || '-', + source: cleanField(change.source) || '-', + notes, + }); + } + + return [...grouped.values()] + .map((group) => ({ + ...group, + rows: group.rows.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()), + })) + .sort((a, b) => new Date(b.latestDate).getTime() - new Date(a.latestDate).getTime()); +} diff --git a/report/kitchen-sink-data.ts b/report/kitchen-sink-data.ts index d4cbab948..b63d0d6b2 100644 --- a/report/kitchen-sink-data.ts +++ b/report/kitchen-sink-data.ts @@ -2,10 +2,10 @@ import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; -import type { ConfigReportData } from './config-types.ts'; +import type { KitchenSinkData } from './kitchen-sink/KitchenSinkTypes.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const raw = readFileSync(resolve(__dirname, 'testdata/kitchen-sink.yaml'), 'utf-8'); -const data = yaml.load(raw) as ConfigReportData; +const data = yaml.load(raw) as KitchenSinkData; export default data; diff --git a/report/kitchen-sink/ApplicationPage.tsx b/report/kitchen-sink/ApplicationPage.tsx new file mode 100644 index 000000000..bb2fe4e68 --- /dev/null +++ b/report/kitchen-sink/ApplicationPage.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import ApplicationDetails from '../components/ApplicationDetails.tsx'; +import AccessControlSection from '../components/AccessControlSection.tsx'; +import IncidentsSection from '../components/IncidentsSection.tsx'; +import BackupsSection from '../components/BackupsSection.tsx'; +import FindingsSection from '../components/FindingsSection.tsx'; +import LocationsSection from '../components/LocationsSection.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function ApplicationPage({ data, pageProps }: Props) { + const app = data.application; + if (!app) return null; + + return ( + +
+
+ Components used in the Application report: details, access control, incidents, backups, findings, and locations. +
+
+ + + + + + + +
+ ); +} diff --git a/report/kitchen-sink/CatalogPage.tsx b/report/kitchen-sink/CatalogPage.tsx new file mode 100644 index 000000000..3dfa797ef --- /dev/null +++ b/report/kitchen-sink/CatalogPage.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import ConfigTreeSection from '../components/ConfigTreeSection.tsx'; +import CatalogAccessSection from '../components/CatalogAccessSection.tsx'; +import CatalogAccessLogsSection from '../components/CatalogAccessLogsSection.tsx'; +import CatalogList from '../components/CatalogList.tsx'; +import ArtifactAppendix from '../components/ArtifactAppendix.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function CatalogPage({ data, pageProps }: Props) { + const catalog = data.catalogReport; + if (!catalog) return null; + + return ( + +
+
+ Components used in the Catalog report: config tree, access control, access logs, catalog list, and artifact appendix. +
+
+ + {catalog.relationshipTree && ( + + )} + + + + + +
+ ); +} diff --git a/report/kitchen-sink/ChangesPage.tsx b/report/kitchen-sink/ChangesPage.tsx new file mode 100644 index 000000000..c9c27c6e7 --- /dev/null +++ b/report/kitchen-sink/ChangesPage.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import ConfigChangesSection from '../components/ConfigChangesSection.tsx'; +import RBACChanges from '../components/RBACChanges.tsx'; +import BackupChanges from '../components/BackupChanges.tsx'; +import DeploymentChanges from '../components/DeploymentChanges.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function ChangesPage({ data, pageProps }: Props) { + const rbacChanges = data.rbacChanges ?? []; + const backupChanges = data.backupChanges ?? []; + const deploymentChanges = data.deploymentChanges ?? []; + + return ( + + + +
+
+ Groups permission changes by config and renders granted/revoked audit rows with role, principal, timestamp, and changed-by attribution. +
+ +
+ +
+
+ Backup calendar/heatmap pattern with event stream. Filters out non-backup change types. +
+ +
+ +
+
+ Highlights deployment-relevant spec, scaling, and policy changes. +
+ +
+
+ ); +} diff --git a/report/kitchen-sink/ConfigComponentsPage.tsx b/report/kitchen-sink/ConfigComponentsPage.tsx new file mode 100644 index 000000000..cf0554dcb --- /dev/null +++ b/report/kitchen-sink/ConfigComponentsPage.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import ConfigLink from '../components/ConfigLink.tsx'; +import ConfigItemCard from '../components/ConfigItemCard.tsx'; +import ScraperCard from '../components/ScraperCard.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function ConfigComponentsPage({ data, pageProps }: Props) { + const sampleConfigs = [data.configItem, ...data.relatedConfigs.slice(0, 5)]; + const scrapers = data.scrapers ?? []; + + return ( + +
+
+ Renders a config item as Icon + Name with optional health indicator. +
+
+ {sampleConfigs.map((config) => ( +
+
+ +
+
+ +
+ {config.type} +
+ ))} +
+
+ +
+
+ Renders a config item with icon, name, tags, and metadata. +
+
+ {sampleConfigs.map((config) => ( +
+ +
+ ))} +
+
+ +
+
+ Renders a scraper with type icons, source badge, spec hash, created by, dates, and GitOps provenance. +
+
+ {scrapers.map((scraper) => ( + + ))} +
+
+
+ ); +} diff --git a/report/kitchen-sink/DynamicSectionsPage.tsx b/report/kitchen-sink/DynamicSectionsPage.tsx new file mode 100644 index 000000000..9e58c447a --- /dev/null +++ b/report/kitchen-sink/DynamicSectionsPage.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import DynamicSection from '../components/DynamicSection.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function DynamicSectionsPage({ data, pageProps }: Props) { + const dynamicSections = data.dynamicSections ?? []; + + return ( + +
+
+ DynamicSection chooses the specialized renderer from the section title and change type mix. +
+
+ + {dynamicSections.map((section, index) => ( + + ))} + + {data.genericChangesSection && ( + <> +
+
+ When changes don't match RBAC/backup/deployment patterns, falls back to a generic table. +
+
+ + + )} + + {data.dynamicViewSection && ( + <> +
+
+ DynamicSection with type='view' renders a table with typed columns. +
+
+ + + )} + + {data.dynamicConfigsSection && ( + <> +
+
+ DynamicSection with type='configs' renders a config list table. +
+
+ + + )} +
+ ); +} diff --git a/report/kitchen-sink/InsightsAndGraphPage.tsx b/report/kitchen-sink/InsightsAndGraphPage.tsx new file mode 100644 index 000000000..bc69af297 --- /dev/null +++ b/report/kitchen-sink/InsightsAndGraphPage.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Page, Section, MatrixTable, Dot } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import ConfigInsightsSection from '../components/ConfigInsightsSection.tsx'; +import ConfigRelationshipGraph from '../components/ConfigRelationshipGraph.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function InsightsAndGraphPage({ data, pageProps }: Props) { + return ( + + + + + +
+
+ Rotated column headers using CSS-Tricks translate+rotate pattern. +
+ alice@example.com, cells: [, , , , null, null] }, + { label: bob@example.com, cells: [, , null, null, null, null] }, + { label: charlie@example.com, cells: [, null, null, null, null, ] }, + { label: deploy-bot, cells: [, , , null, null, null] }, + { label: monitoring-svc, cells: [, null, null, null, null, ] }, + ]} + /> +
+ With longer column names and more rows. +
+ design-studio-pas, cells: [null, null, , null, null, null, null] }, + { label: monitoring_ro, cells: [, null, null, null, null, null, null] }, + { label: oipa-qa-bot, cells: [null, null, , null, null, null, null] }, + { label: omasa, cells: [null, null, , null, null, null, null] }, + { label: SG-OMAR Shared Dev DB, cells: [null, , null, null, , null, null] }, + { label: SG-OMAR Shared RO, cells: [, null, null, null, null, null, ] }, + { label: svc_mission_control, cells: [, , null, null, null, null, null] }, + ]} + /> +
+
+ ); +} diff --git a/report/kitchen-sink/KitchenSinkTypes.ts b/report/kitchen-sink/KitchenSinkTypes.ts new file mode 100644 index 000000000..2feffa61e --- /dev/null +++ b/report/kitchen-sink/KitchenSinkTypes.ts @@ -0,0 +1,21 @@ +import type { ConfigReportData } from '../config-types.ts'; +import type { ApplicationChange, ApplicationSection, Application } from '../types.ts'; +import type { RBACReport } from '../rbac-types.ts'; +import type { CatalogReportData } from '../catalog-report-types.ts'; +import type { ViewReportData } from '../view-types.ts'; +import type { ScraperInfo } from '../scraper-types.ts'; + +export interface KitchenSinkData extends ConfigReportData { + rbacChanges?: ApplicationChange[]; + backupChanges?: ApplicationChange[]; + deploymentChanges?: ApplicationChange[]; + dynamicSections?: ApplicationSection[]; + genericChangesSection?: ApplicationSection; + dynamicViewSection?: ApplicationSection; + dynamicConfigsSection?: ApplicationSection; + scrapers?: ScraperInfo[]; + application?: Application; + rbacReport?: RBACReport; + catalogReport?: Partial; + viewReport?: ViewReportData; +} diff --git a/report/kitchen-sink/LayoutComponentsPage.tsx b/report/kitchen-sink/LayoutComponentsPage.tsx new file mode 100644 index 000000000..925726d1b --- /dev/null +++ b/report/kitchen-sink/LayoutComponentsPage.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import CoverPage from '../components/CoverPage.tsx'; +import PageHeaderComponent from '../components/PageHeader.tsx'; +import PageFooterComponent from '../components/PageFooter.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function LayoutComponentsPage({ data, pageProps }: Props) { + const config = data.configItem; + + return ( + +
+
+ Reusable cover page component with title, icon, breadcrumbs, subjects, tags, stats, and date range. +
+
+ +
+
+ +
+
+ Logo-based header bar with optional subtitle. +
+
+
+ +
+
+ +
+
+
+ +
+
+ Footer with generation timestamp and optional public URL. +
+
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/report/kitchen-sink/RBACPage.tsx b/report/kitchen-sink/RBACPage.tsx new file mode 100644 index 000000000..d5d1c84ea --- /dev/null +++ b/report/kitchen-sink/RBACPage.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import RBACCoverContent from '../components/RBACCoverContent.tsx'; +import RBACSummarySection from '../components/RBACSummarySection.tsx'; +import RBACMatrixSection from '../components/RBACMatrixSection.tsx'; +import RBACUserSection from '../components/RBACUserSection.tsx'; +import RBACChangelogSection from '../components/RBACChangelogSection.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function RBACPage({ data, pageProps }: Props) { + const report = data.rbacReport; + if (!report) return null; + + return ( + +
+
+ Components used in the RBAC reports: cover, summary, matrix, per-user view, and changelog. +
+
+ +
+
+ RBAC-specific cover page with subject, breadcrumbs, and summary stats. +
+
+ +
+
+ + + + {(report.resources || []).map((resource) => ( + + ))} + + {(report.users || []).map((user) => ( +
+ +
+ ))} + + +
+ ); +} diff --git a/report/kitchen-sink/ViewPage.tsx b/report/kitchen-sink/ViewPage.tsx new file mode 100644 index 000000000..351823d56 --- /dev/null +++ b/report/kitchen-sink/ViewPage.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Page, Section } from '@flanksource/facet'; +import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import ViewResultSection from '../components/ViewResultSection.tsx'; + +interface Props { + data: KitchenSinkData; + pageProps: any; +} + +export default function ViewPage({ data, pageProps }: Props) { + const viewReport = data.viewReport; + if (!viewReport) return null; + + return ( + +
+
+ Renders view data with typed columns (string, number, boolean, datetime, duration, + health, status, gauge, bytes, millicore, config_item, labels) and panels + (number, gauge, bargauge, piechart, table, text). +
+
+ +
+ ); +} diff --git a/report/testdata/kitchen-sink.yaml b/report/testdata/kitchen-sink.yaml index 8b67ae563..16f86037a 100644 --- a/report/testdata/kitchen-sink.yaml +++ b/report/testdata/kitchen-sink.yaml @@ -214,7 +214,7 @@ analyses: - id: "ana-008" configID: "cfg-eks-001" analyzer: "AWS Best Practices" - message: "EKS cluster running version 1.29 — version 1.30 is available with security patches" + message: "EKS cluster running version 1.29 - version 1.30 is available with security patches" status: "open" severity: "info" analysisType: "recommendation" @@ -236,7 +236,7 @@ analyses: - id: "ana-010" configID: "cfg-eks-001" analyzer: "Prometheus Advisor" - message: "Node ip-10-0-2-18 memory utilization consistently above 85% — risk of OOM kills" + message: "Node ip-10-0-2-18 memory utilization consistently above 85% - risk of OOM kills" status: "open" severity: "high" analysisType: "reliability" @@ -391,3 +391,1226 @@ relatedConfigs: health: "warning" labels: instance-type: "i3.xlarge" + +rbacChanges: + - id: "rbac-001" + date: "2026-03-30T09:12:00Z" + changeType: "PermissionAdded" + source: "azure-entra" + createdBy: "alice@flanksource.com" + configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + permission: + user: "alice@flanksource.com" + role: "db_owner" + group: "incident-responders" + description: "PermissionAdded: user alice@flanksource.com, role db_owner, group incident-responders" + status: "info" + createdAt: "2026-03-30T09:12:00Z" + + - id: "rbac-002" + date: "2026-03-29T18:40:00Z" + changeType: "PermissionRemoved" + source: "azure-entra" + createdBy: "security-automation" + configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + permission: + user: "contractor-temp" + role: "db_datareader" + description: "PermissionRemoved: user contractor-temp, role db_datareader" + status: "info" + createdAt: "2026-03-29T18:40:00Z" + + - id: "rbac-003" + date: "2026-03-29T16:00:00Z" + changeType: "AccessReviewed" + source: "access-review-job" + createdBy: "governance-bot" + configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + description: "Quarterly review completed for production database roles" + status: "info" + createdAt: "2026-03-29T16:00:00Z" + + - id: "rbac-004" + date: "2026-03-28T13:05:00Z" + changeType: "PermissionGranted" + source: "okta" + createdBy: "bob@flanksource.com" + configId: "cfg-analytics-001" + configName: "analytics-db" + configType: "MSSQL::Database" + description: "Granted db_ddladmin to deploy-bot on analytics-db" + status: "info" + createdAt: "2026-03-28T13:05:00Z" + + - id: "rbac-005" + date: "2026-03-27T07:20:00Z" + changeType: "PermissionAdded" + source: "okta" + configId: "cfg-keyvault-001" + configName: "prod-keyvault" + configType: "Azure::KeyVault" + permission: + group: "break-glass-admins" + role: "Secrets Officer" + description: "PermissionAdded: role Secrets Officer, group break-glass-admins" + status: "info" + createdAt: "2026-03-27T07:20:00Z" + +backupChanges: + - id: "bak-001" + date: "2026-03-30T02:00:00Z" + changeType: "BackupStarted" + source: "aws-backup" + description: "Nightly snapshot started for incident-commander-db" + status: "info" + createdAt: "2026-03-30T02:00:00Z" + + - id: "bak-002" + date: "2026-03-30T02:08:00Z" + changeType: "BackupCompleted" + source: "aws-backup" + description: "Nightly snapshot completed for incident-commander-db (4.3 GB)" + status: "info" + createdAt: "2026-03-30T02:08:00Z" + + - id: "bak-003" + date: "2026-03-29T02:01:00Z" + changeType: "BackupFailed" + source: "aws-backup" + description: "Snapshot failed for incident-commander-db after storage timeout" + status: "high" + createdAt: "2026-03-29T02:01:00Z" + + - id: "bak-004" + date: "2026-03-28T12:10:00Z" + changeType: "BackupRestored" + source: "drill-playbook" + createdBy: "platform-oncall" + description: "Restored staging copy from nightly snapshot for disaster recovery drill" + status: "info" + createdAt: "2026-03-28T12:10:00Z" + + - id: "bak-005" + date: "2026-03-28T12:18:00Z" + changeType: "RestoreCompleted" + source: "drill-playbook" + createdBy: "platform-oncall" + description: "Restore completed and validation checks passed" + status: "info" + createdAt: "2026-03-28T12:18:00Z" + + - id: "bak-006" + date: "2026-03-27T02:00:00Z" + changeType: "BackupEnqueued" + source: "aws-backup" + description: "Queued backup job for archive-postgres" + status: "info" + createdAt: "2026-03-27T02:00:00Z" + + - id: "bak-007" + date: "2026-03-27T10:00:00Z" + changeType: "diff" + source: "terraform" + description: "This diff should be filtered out of the backup-focused renderer" + status: "low" + createdAt: "2026-03-27T10:00:00Z" + +deploymentChanges: + - id: "dep-001" + date: "2026-03-30T08:15:00Z" + changeType: "diff" + source: "argocd" + createdBy: "deploy-bot" + description: "Deployment incident-commander image updated: v1.4.199 -> v1.4.200" + status: "low" + createdAt: "2026-03-30T08:15:00Z" + + - id: "dep-002" + date: "2026-03-30T07:30:00Z" + changeType: "Pulled" + source: "kubernetes" + description: "Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42" + status: "info" + createdAt: "2026-03-30T07:30:00Z" + + - id: "dep-003" + date: "2026-03-29T22:00:00Z" + changeType: "ScalingReplicaSet" + source: "kubernetes" + createdBy: "cluster-autoscaler" + description: "Deployment incident-commander scaled from 2 to 4 replicas" + status: "low" + createdAt: "2026-03-29T22:00:00Z" + + - id: "dep-004" + date: "2026-03-29T14:00:00Z" + changeType: "PolicyUpdate" + source: "argocd" + createdBy: "alice@flanksource.com" + description: "Deployment network policy updated to restrict egress to approved CIDRs" + status: "medium" + createdAt: "2026-03-29T14:00:00Z" + + - id: "dep-005" + date: "2026-03-28T10:00:00Z" + changeType: "FieldsV1" + source: "kubernetes" + description: "FieldsV1 payload updated during reconciliation" + status: "info" + createdAt: "2026-03-28T10:00:00Z" + + - id: "dep-006" + date: "2026-03-27T09:00:00Z" + changeType: "diff" + source: "terraform" + createdBy: "carol@flanksource.com" + description: "Deployment incident-commander rollout template updated with new topology spread constraints" + status: "medium" + createdAt: "2026-03-27T09:00:00Z" + +scrapers: + - id: "scr-001" + name: "mc/aws-production" + namespace: "mc" + source: "KubernetesCRD" + types: ["aws", "kubernetes"] + specHash: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd" + createdBy: "alice@flanksource.com" + createdAt: "2025-06-10T09:00:00Z" + updatedAt: "2026-03-28T14:30:00Z" + gitops: + git: + url: "https://github.com/flanksource/mission-control-demo" + branch: "main" + file: "clusters/prod/scrapers/aws.yaml" + dir: "clusters/prod/scrapers" + link: "https://github.com/flanksource/mission-control-demo/tree/main/clusters/prod/scrapers/aws.yaml" + kustomize: + path: "clusters/prod/scrapers" + file: "clusters/prod/scrapers/kustomization.yaml" + - id: "scr-002" + name: "mc/azure-entra" + namespace: "mc" + source: "KubernetesCRD" + types: ["azure"] + specHash: "ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33" + createdAt: "2025-09-01T10:00:00Z" + updatedAt: "2026-03-30T08:00:00Z" + - id: "scr-003" + name: "local-file-scraper" + source: "ConfigFile" + types: ["file", "sql"] + specHash: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + createdBy: "bob@flanksource.com" + createdAt: "2026-01-15T11:00:00Z" + +genericChangesSection: + type: "changes" + title: "Recent Infrastructure Changes" + changes: + - id: "gen-001" + date: "2026-03-30T10:00:00Z" + changeType: "ConfigUpdate" + source: "terraform" + createdBy: "alice@flanksource.com" + description: "Updated VPC CIDR block from 10.0.0.0/16 to 10.0.0.0/12" + status: "medium" + createdAt: "2026-03-30T10:00:00Z" + - id: "gen-002" + date: "2026-03-29T15:00:00Z" + changeType: "TagUpdate" + source: "aws-config" + description: "Added cost-center tag to 14 resources in us-east-1" + status: "info" + createdAt: "2026-03-29T15:00:00Z" + - id: "gen-003" + date: "2026-03-28T09:00:00Z" + changeType: "SecurityGroupChange" + source: "aws-config" + createdBy: "security-automation" + description: "Removed unused ingress rule on sg-0abc123 (port 8080)" + status: "low" + createdAt: "2026-03-28T09:00:00Z" + - id: "gen-004" + date: "2026-03-27T14:00:00Z" + changeType: "DNSUpdate" + source: "route53" + createdBy: "bob@flanksource.com" + description: "Added CNAME record api-v2.flanksource.com -> prod-alb.us-east-1.elb.amazonaws.com" + status: "info" + createdAt: "2026-03-27T14:00:00Z" + +dynamicViewSection: + type: "view" + title: "Cluster Nodes" + view: + columns: + - name: "node" + type: "string" + - name: "status" + type: "status" + - name: "health" + type: "health" + - name: "cpu_used" + type: "gauge" + gauge: + thresholds: + - percent: 0 + color: "#22C55E" + - percent: 70 + color: "#EAB308" + - percent: 90 + color: "#EF4444" + - name: "memory" + type: "bytes" + rows: + - ["ip-10-0-1-42", "Ready", "healthy", 45.2, 8589934592] + - ["ip-10-0-2-18", "Ready", "warning", 87.3, 14495514624] + - ["ip-10-0-3-7", "NotReady", "unhealthy", 0, 0] + +dynamicConfigsSection: + type: "configs" + title: "Related Databases" + configs: + - id: "db-001" + name: "mission-control-db" + type: "AWS::RDS::Instance" + status: "available" + health: "healthy" + labels: + engine: "postgresql" + env: "production" + - id: "db-002" + name: "analytics-db" + type: "AWS::RDS::Instance" + status: "available" + health: "warning" + labels: + engine: "postgresql" + env: "production" + - id: "db-003" + name: "archive-db" + type: "AWS::RDS::Instance" + status: "stopped" + health: "unknown" + labels: + engine: "postgresql" + env: "staging" + +application: + id: "app-001" + name: "Mission Control" + type: "WebApplication" + namespace: "mc" + description: "Internal developer platform for Kubernetes fleet management" + properties: + - name: "uptime" + label: "Uptime" + value: 99.97 + unit: "percentage" + order: 1 + - name: "latency" + label: "P99 Latency" + value: 245 + unit: "milliseconds" + order: 2 + - name: "requests" + label: "Requests/s" + value: 1240 + order: 3 + - name: "error_rate" + label: "Error Rate" + value: 0.03 + unit: "percentage" + order: 4 + accessControl: + users: + - id: "u-001" + name: "Alice Johnson" + email: "alice@flanksource.com" + role: "admin" + authType: "SSO" + created: "2025-01-15T09:00:00Z" + lastLogin: "2026-03-30T08:00:00Z" + lastAccessReview: "2026-03-15T10:00:00Z" + - id: "u-002" + name: "Bob Smith" + email: "bob@flanksource.com" + role: "editor" + authType: "SSO" + created: "2025-06-01T09:00:00Z" + lastLogin: "2026-03-28T14:00:00Z" + lastAccessReview: "2026-02-01T10:00:00Z" + - id: "u-003" + name: "Carol Davis" + email: "carol@flanksource.com" + role: "viewer" + authType: "API Key" + created: "2026-01-10T09:00:00Z" + lastLogin: null + lastAccessReview: null + authentication: + - name: "Azure AD SSO" + type: "SAML" + mfa: + type: "TOTP" + enforced: "true" + properties: + tenant: "flanksource.onmicrosoft.com" + - name: "API Key Auth" + type: "Bearer" + mfa: + type: "none" + enforced: "false" + properties: + rotation: "90 days" + incidents: + - id: "inc-001" + date: "2026-03-28T03:15:00Z" + severity: "critical" + description: "Database connection pool exhausted causing 503 errors across all API endpoints" + status: "resolved" + resolvedDate: "2026-03-28T04:45:00Z" + - id: "inc-002" + date: "2026-03-25T14:00:00Z" + severity: "high" + description: "Config scraper failing to sync AWS resources due to expired IAM credentials" + status: "resolved" + resolvedDate: "2026-03-25T15:30:00Z" + - id: "inc-003" + date: "2026-03-22T09:30:00Z" + severity: "medium" + description: "Notification delivery delayed by 15+ minutes due to queue backlog" + status: "resolved" + resolvedDate: "2026-03-22T11:00:00Z" + - id: "inc-004" + date: "2026-03-30T06:00:00Z" + severity: "low" + description: "Health check dashboard showing stale data for canary-checker pods" + status: "open" + - id: "inc-005" + date: "2026-03-29T20:00:00Z" + severity: "high" + description: "Memory leak in event processor causing gradual degradation" + status: "open" + locations: + - account: "flanksource-prod" + name: "us-east-1-primary" + type: "EKS Cluster" + purpose: "primary" + region: "us-east-1" + provider: "AWS" + resourceCount: 142 + - account: "flanksource-prod" + name: "eu-west-1-backup" + type: "EKS Cluster" + purpose: "backup" + region: "eu-west-1" + provider: "AWS" + resourceCount: 38 + - account: "flanksource-dr" + name: "us-west-2-dr" + type: "EKS Cluster" + purpose: "dr" + region: "us-west-2" + provider: "AWS" + resourceCount: 15 + backups: + - id: "bkp-001" + database: "mission-control-db" + type: "snapshot" + source: "aws-backup" + date: "2026-03-30T02:00:00Z" + size: "4.3 GB" + status: "success" + - id: "bkp-002" + database: "mission-control-db" + type: "snapshot" + source: "aws-backup" + date: "2026-03-29T02:00:00Z" + size: "4.2 GB" + status: "failed" + - id: "bkp-003" + database: "mission-control-db" + type: "snapshot" + source: "aws-backup" + date: "2026-03-28T02:00:00Z" + size: "4.1 GB" + status: "success" + - id: "bkp-004" + database: "analytics-db" + type: "logical" + source: "pg_dump" + date: "2026-03-30T03:00:00Z" + size: "1.8 GB" + status: "success" + - id: "bkp-005" + database: "analytics-db" + type: "logical" + source: "pg_dump" + date: "2026-03-29T03:00:00Z" + size: "1.7 GB" + status: "in-progress" + restores: + - id: "rst-001" + database: "mission-control-db" + date: "2026-03-28T12:00:00Z" + source: "aws-backup" + status: "success" + completedAt: "2026-03-28T12:18:00Z" + - id: "rst-002" + database: "analytics-db" + date: "2026-03-15T09:00:00Z" + source: "pg_dump" + status: "success" + completedAt: "2026-03-15T09:35:00Z" + findings: + - id: "find-001" + type: "security" + severity: "critical" + title: "CVE-2026-0891 in libcrypto" + description: "Critical vulnerability in OpenSSL library" + date: "2026-03-25T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "open" + remediation: "Upgrade base image to golang:1.23.1-alpine" + - id: "find-002" + type: "security" + severity: "high" + title: "Container running as root" + description: "incident-commander pod runs as UID 0" + date: "2026-03-20T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "open" + remediation: "Add securityContext.runAsNonRoot to deployment spec" + - id: "find-003" + type: "compliance" + severity: "medium" + title: "Missing data-classification label" + description: "Namespace mc missing required label" + date: "2026-03-15T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "open" + remediation: "Add label data-classification=internal to namespace" + - id: "find-004" + type: "compliance" + severity: "low" + title: "Pod disruption budget missing" + description: "canary-checker deployment has no PDB" + date: "2026-03-10T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "accepted" + - id: "find-005" + type: "reliability" + severity: "high" + title: "Node memory pressure" + description: "ip-10-0-2-18 consistently above 85% memory" + date: "2026-03-26T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "open" + remediation: "Scale node group or add memory limits to workloads" + - id: "find-006" + type: "reliability" + severity: "medium" + title: "Single replica deployment" + description: "config-db running with 1 replica" + date: "2026-03-01T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "resolved" + - id: "find-007" + type: "performance" + severity: "high" + title: "API latency above threshold" + description: "P99 latency exceeded 500ms 12 times in 7 days" + date: "2026-03-23T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "in-progress" + remediation: "Investigate slow queries and add connection pooling" + - id: "find-008" + type: "performance" + severity: "low" + title: "Slow config scraper sync" + description: "AWS scraper taking >10min per cycle" + date: "2026-03-18T09:00:00Z" + lastObserved: "2026-03-30T09:00:00Z" + status: "open" + sections: [] + +rbacReport: + title: "prod-sql-primary" + query: "type=MSSQL::Database AND name=prod-sql-primary" + generatedAt: "2026-03-30T12:00:00Z" + subject: + id: "cfg-sql-001" + name: "prod-sql-primary" + type: "MSSQL::Database" + config_class: "Database" + status: "Online" + health: "healthy" + description: "Primary SQL Server database for production workloads" + tags: + env: "production" + team: "data-platform" + parents: + - id: "cfg-sql-server-001" + name: "sql-prod-east" + type: "MSSQL::Server" + - id: "cfg-rg-001" + name: "rg-prod-data" + type: "Azure::ResourceGroup" + summary: + totalUsers: 8 + totalResources: 2 + staleAccessCount: 2 + overdueReviews: 1 + directAssignments: 6 + groupAssignments: 4 + resources: + - configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + configClass: "Database" + path: "rg-prod-data.sql-prod-east.prod-sql-primary" + status: "Online" + health: "healthy" + tags: + env: "production" + labels: + team: "data-platform" + users: + - userId: "u-alice" + userName: "alice@flanksource.com" + email: "alice@flanksource.com" + role: "db_owner" + roleSource: "direct" + sourceSystem: "azure-entra" + createdAt: "2025-01-15T09:00:00Z" + lastSignedInAt: "2026-03-30T08:00:00Z" + lastReviewedAt: "2026-03-15T10:00:00Z" + isStale: false + isReviewOverdue: false + - userId: "u-bob" + userName: "bob@flanksource.com" + email: "bob@flanksource.com" + role: "db_datareader" + roleSource: "direct" + sourceSystem: "azure-entra" + createdAt: "2025-06-01T09:00:00Z" + lastSignedInAt: "2026-03-28T14:00:00Z" + lastReviewedAt: "2026-02-01T10:00:00Z" + isStale: false + isReviewOverdue: false + - userId: "u-bob" + userName: "bob@flanksource.com" + email: "bob@flanksource.com" + role: "db_datawriter" + roleSource: "group:SG-DataEngineers" + sourceSystem: "azure-entra" + createdAt: "2025-06-01T09:00:00Z" + lastSignedInAt: "2026-03-28T14:00:00Z" + lastReviewedAt: "2026-02-01T10:00:00Z" + isStale: false + isReviewOverdue: false + - userId: "u-carol" + userName: "carol@flanksource.com" + email: "carol@flanksource.com" + role: "db_datareader" + roleSource: "group:SG-Analytics" + sourceSystem: "azure-entra" + createdAt: "2026-01-10T09:00:00Z" + lastSignedInAt: null + lastReviewedAt: null + isStale: true + isReviewOverdue: true + - userId: "u-deploy-bot" + userName: "deploy-bot" + email: "deploy-bot@flanksource.com" + role: "db_ddladmin" + roleSource: "direct" + sourceSystem: "azure-entra" + createdAt: "2025-08-01T09:00:00Z" + lastSignedInAt: "2026-03-30T06:00:00Z" + lastReviewedAt: "2026-03-01T10:00:00Z" + isStale: false + isReviewOverdue: false + - userId: "u-contractor" + userName: "contractor-temp" + email: "contractor@external.com" + role: "db_datareader" + roleSource: "direct" + sourceSystem: "okta" + createdAt: "2025-12-01T09:00:00Z" + lastSignedInAt: "2025-12-15T10:00:00Z" + lastReviewedAt: null + isStale: true + isReviewOverdue: true + changelog: + - configId: "cfg-sql-001" + date: "2026-03-30T09:12:00Z" + changeType: "PermissionGranted" + user: "alice@flanksource.com" + role: "db_owner" + configName: "prod-sql-primary" + source: "azure-entra" + description: "Granted during oncall rotation" + - configId: "cfg-sql-001" + date: "2026-03-29T18:40:00Z" + changeType: "PermissionRevoked" + user: "contractor-temp" + role: "db_datareader" + configName: "prod-sql-primary" + source: "azure-entra" + description: "Contract ended" + changelog: + - configId: "cfg-sql-001" + date: "2026-03-30T09:12:00Z" + changeType: "PermissionGranted" + user: "alice@flanksource.com" + role: "db_owner" + configName: "prod-sql-primary" + source: "azure-entra" + description: "Granted during oncall rotation" + - configId: "cfg-sql-001" + date: "2026-03-29T18:40:00Z" + changeType: "PermissionRevoked" + user: "contractor-temp" + role: "db_datareader" + configName: "prod-sql-primary" + source: "azure-entra" + description: "Contract ended" + - configId: "cfg-sql-001" + date: "2026-03-29T16:00:00Z" + changeType: "AccessReviewed" + user: "governance-bot" + role: "all" + configName: "prod-sql-primary" + source: "access-review-job" + description: "Quarterly review completed" + - configId: "cfg-sql-001" + date: "2026-03-28T13:05:00Z" + changeType: "PermissionGranted" + user: "deploy-bot" + role: "db_ddladmin" + configName: "prod-sql-primary" + source: "azure-entra" + description: "Automated pipeline access" + - configId: "cfg-sql-001" + date: "2026-03-15T10:00:00Z" + changeType: "PermissionGranted" + user: "bob@flanksource.com" + role: "db_datawriter" + configName: "prod-sql-primary" + source: "azure-entra" + description: "Added via SG-DataEngineers group" + - configId: "cfg-sql-001" + date: "2026-03-01T09:00:00Z" + changeType: "PermissionRevoked" + user: "intern-2025" + role: "db_datareader" + configName: "prod-sql-primary" + source: "okta" + description: "Internship ended" + users: + - userId: "u-alice" + userName: "alice@flanksource.com" + email: "alice@flanksource.com" + sourceSystem: "azure-entra" + lastSignedInAt: "2026-03-30T08:00:00Z" + resources: + - configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + configClass: "Database" + role: "db_owner" + roleSource: "direct" + createdAt: "2025-01-15T09:00:00Z" + lastSignedInAt: "2026-03-30T08:00:00Z" + lastReviewedAt: "2026-03-15T10:00:00Z" + isStale: false + isReviewOverdue: false + - configId: "cfg-sql-002" + configName: "analytics-db" + configType: "MSSQL::Database" + configClass: "Database" + role: "db_datareader" + roleSource: "group:SG-Analytics" + createdAt: "2025-03-01T09:00:00Z" + lastSignedInAt: "2026-03-28T14:00:00Z" + lastReviewedAt: "2026-03-15T10:00:00Z" + isStale: false + isReviewOverdue: false + - userId: "u-bob" + userName: "bob@flanksource.com" + email: "bob@flanksource.com" + sourceSystem: "azure-entra" + lastSignedInAt: "2026-03-28T14:00:00Z" + resources: + - configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + configClass: "Database" + role: "db_datareader" + roleSource: "direct" + createdAt: "2025-06-01T09:00:00Z" + lastSignedInAt: "2026-03-28T14:00:00Z" + lastReviewedAt: "2026-02-01T10:00:00Z" + isStale: false + isReviewOverdue: false + - configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + configClass: "Database" + role: "db_datawriter" + roleSource: "group:SG-DataEngineers" + createdAt: "2025-06-01T09:00:00Z" + lastSignedInAt: "2026-03-28T14:00:00Z" + lastReviewedAt: "2026-02-01T10:00:00Z" + isStale: false + isReviewOverdue: false + - configId: "cfg-kv-001" + configName: "prod-keyvault" + configType: "Azure::KeyVault" + configClass: "Security" + role: "Secrets Reader" + roleSource: "direct" + createdAt: "2025-09-01T09:00:00Z" + lastSignedInAt: "2026-03-25T10:00:00Z" + lastReviewedAt: "2026-01-15T10:00:00Z" + isStale: false + isReviewOverdue: false + +catalogReport: + relationshipTree: + id: "cfg-eks-001" + name: "prod-eks-cluster" + type: "AWS::EKS::Cluster" + edgeType: "target" + children: + - id: "cfg-vpc-001" + name: "prod-vpc" + type: "AWS::EC2::VPC" + edgeType: "parent" + relation: "RunsIn" + children: + - id: "cfg-subnet-001" + name: "private-subnet-1a" + type: "AWS::EC2::Subnet" + edgeType: "child" + - id: "cfg-subnet-002" + name: "private-subnet-1b" + type: "AWS::EC2::Subnet" + edgeType: "child" + - id: "cfg-ns-001" + name: "mc" + type: "Kubernetes::Namespace" + edgeType: "child" + relation: "ChildOf" + children: + - id: "cfg-deploy-001" + name: "incident-commander" + type: "Kubernetes::Deployment" + edgeType: "child" + - id: "cfg-deploy-002" + name: "canary-checker" + type: "Kubernetes::Deployment" + edgeType: "child" + - id: "cfg-rds-001" + name: "mission-control-db" + type: "AWS::RDS::Instance" + edgeType: "related" + relation: "DependsOn" + access: + - userId: "u-alice" + userName: "Alice Johnson" + email: "alice@flanksource.com" + role: "admin" + userType: "User" + createdAt: "2025-01-15T09:00:00Z" + lastSignedInAt: "2026-03-30T08:00:00Z" + lastReviewedAt: "2026-03-15T10:00:00Z" + - userId: "u-bob" + userName: "Bob Smith" + email: "bob@flanksource.com" + role: "editor" + userType: "User" + createdAt: "2025-06-01T09:00:00Z" + lastSignedInAt: "2026-03-28T14:00:00Z" + lastReviewedAt: "2026-02-01T10:00:00Z" + - userId: "u-carol" + userName: "Carol Davis" + email: "carol@flanksource.com" + role: "viewer" + userType: "User" + createdAt: "2026-01-10T09:00:00Z" + lastSignedInAt: null + - userId: "u-deploy-bot" + userName: "deploy-bot" + email: "deploy-bot@flanksource.com" + role: "editor" + userType: "ServiceAccount" + createdAt: "2025-08-01T09:00:00Z" + lastSignedInAt: "2026-03-30T06:00:00Z" + lastReviewedAt: "2026-03-01T10:00:00Z" + - userId: "u-stale" + userName: "Former Employee" + email: "former@flanksource.com" + role: "viewer" + userType: "User" + createdAt: "2024-06-01T09:00:00Z" + lastSignedInAt: "2025-06-15T10:00:00Z" + accessLogs: + - userId: "u-alice" + userName: "Alice Johnson" + configName: "prod-eks-cluster" + configType: "AWS::EKS::Cluster" + createdAt: "2026-03-30T08:15:00Z" + mfa: true + count: 3 + properties: + action: "describe-cluster" + source: "kubectl" + - userId: "u-bob" + userName: "Bob Smith" + configName: "prod-eks-cluster" + configType: "AWS::EKS::Cluster" + createdAt: "2026-03-29T14:30:00Z" + mfa: true + count: 1 + - userId: "u-deploy-bot" + userName: "deploy-bot" + configName: "prod-eks-cluster" + configType: "AWS::EKS::Cluster" + createdAt: "2026-03-30T06:00:00Z" + mfa: false + count: 12 + properties: + action: "apply" + source: "argocd" + - userId: "u-carol" + userName: "Carol Davis" + configName: "prod-eks-cluster" + configType: "AWS::EKS::Cluster" + createdAt: "2026-03-25T11:00:00Z" + mfa: false + count: 1 + - userId: "u-alice" + userName: "Alice Johnson" + configName: "mission-control-db" + configType: "AWS::RDS::Instance" + createdAt: "2026-03-30T09:00:00Z" + mfa: true + count: 2 + properties: + action: "connect" + source: "psql" + - userId: "u-stale" + userName: "Former Employee" + configName: "prod-eks-cluster" + configType: "AWS::EKS::Cluster" + createdAt: "2025-06-15T10:00:00Z" + mfa: false + count: 1 + entries: + - configItem: + id: "cfg-deploy-001" + name: "incident-commander" + type: "Kubernetes::Deployment" + status: "Running" + health: "healthy" + changeCount: 5 + insightCount: 3 + accessCount: 2 + changes: [] + analyses: [] + access: [] + accessLogs: [] + - configItem: + id: "cfg-deploy-002" + name: "canary-checker" + type: "Kubernetes::Deployment" + status: "Running" + health: "healthy" + changeCount: 2 + insightCount: 1 + accessCount: 1 + changes: [] + analyses: [] + access: [] + accessLogs: [] + - configItem: + id: "cfg-deploy-003" + name: "config-db" + type: "Kubernetes::Deployment" + status: "Running" + health: "unhealthy" + changeCount: 1 + insightCount: 2 + accessCount: 0 + changes: [] + analyses: [] + access: [] + accessLogs: [] + changes: + - id: "art-001" + configID: "cfg-deploy-001" + configName: "incident-commander" + configType: "Kubernetes::Deployment" + changeType: "diff" + severity: "medium" + source: "argocd" + summary: "Deployment spec updated with new resource limits" + createdAt: "2026-03-30T08:15:00Z" + artifacts: + - id: "a-001" + filename: "diff-screenshot.png" + contentType: "image/png" + size: 45000 + dataUri: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + - id: "art-002" + configID: "cfg-deploy-001" + configName: "incident-commander" + configType: "Kubernetes::Deployment" + changeType: "PolicyUpdate" + severity: "high" + source: "argocd" + summary: "Network policy tightened for egress" + createdAt: "2026-03-29T14:00:00Z" + artifacts: + - id: "a-002" + filename: "policy-diff.yaml" + contentType: "text/yaml" + size: 1200 + +viewReport: + name: "cluster-overview" + title: "Cluster Overview" + icon: "AWS::EKS::Cluster" + columns: + - name: "name" + type: "string" + - name: "type" + type: "string" + - name: "health" + type: "health" + - name: "status" + type: "status" + - name: "cpu" + type: "gauge" + gauge: + thresholds: + - percent: 0 + color: "#22C55E" + - percent: 70 + color: "#EAB308" + - percent: 90 + color: "#EF4444" + - name: "memory" + type: "bytes" + - name: "uptime" + type: "duration" + - name: "requests" + type: "number" + unit: "req/s" + - name: "ready" + type: "boolean" + - name: "last_seen" + type: "datetime" + - name: "labels" + type: "labels" + rows: + - ["incident-commander", "Deployment", "healthy", "Running", 42.5, 536870912, 86400000000000, 1240, true, "2026-03-30T08:00:00Z", {"app": "incident-commander", "env": "production"}] + - ["canary-checker", "Deployment", "healthy", "Running", 18.3, 268435456, 172800000000000, 450, true, "2026-03-30T08:00:00Z", {"app": "canary-checker", "env": "production"}] + - ["config-db", "Deployment", "unhealthy", "CrashLoopBackOff", 92.1, 1073741824, 3600000000000, 0, false, "2026-03-30T07:45:00Z", {"app": "config-db", "env": "production"}] + - ["cert-manager", "Deployment", "healthy", "Running", 5.2, 134217728, 604800000000000, 12, true, "2026-03-30T08:00:00Z", {"app": "cert-manager"}] + - ["nginx-ingress", "Deployment", "warning", "Running", 78.9, 805306368, 259200000000000, 3200, true, "2026-03-30T08:00:00Z", {"app": "nginx-ingress", "env": "production"}] + panels: + - name: "Total Requests" + type: "number" + number: + unit: "req/s" + rows: + - value: 4902 + - name: "CPU Utilization" + type: "gauge" + gauge: + unit: "%" + thresholds: + - percent: 0 + color: "#22C55E" + - percent: 70 + color: "#EAB308" + - percent: 90 + color: "#EF4444" + rows: + - value: 47.4 + - name: "Pod Distribution" + type: "piechart" + piechart: + showLabels: true + colors: + healthy: "#22C55E" + warning: "#EAB308" + unhealthy: "#EF4444" + rows: + - name: "healthy" + value: 12 + - name: "warning" + value: 3 + - name: "unhealthy" + value: 1 + - name: "Memory by Service" + type: "bargauge" + bargauge: + unit: "bytes" + max: 2147483648 + thresholds: + - percent: 0 + color: "#3B82F6" + - percent: 70 + color: "#EAB308" + - percent: 90 + color: "#EF4444" + rows: + - name: "incident-commander" + value: 536870912 + - name: "config-db" + value: 1073741824 + - name: "nginx-ingress" + value: 805306368 + - name: "canary-checker" + value: 268435456 + - name: "Cluster Status" + type: "text" + rows: + - value: "All critical services operational. config-db pod in CrashLoopBackOff - investigating OOM kills." + - name: "Recent Deployments" + type: "table" + rows: + - service: "incident-commander" + version: "v1.4.200" + deployed: "2026-03-30T08:15:00Z" + status: "success" + - service: "canary-checker" + version: "v1.0.350" + deployed: "2026-03-29T12:00:00Z" + status: "success" + - service: "config-db" + version: "v2.1.0" + deployed: "2026-03-30T07:30:00Z" + status: "failed" + +dynamicSections: + - type: "changes" + title: "Permissions Added / Removed" + changes: + - id: "rbac-001" + date: "2026-03-30T09:12:00Z" + changeType: "PermissionAdded" + source: "azure-entra" + createdBy: "alice@flanksource.com" + configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + permission: + user: "alice@flanksource.com" + role: "db_owner" + group: "incident-responders" + description: "PermissionAdded: user alice@flanksource.com, role db_owner, group incident-responders" + status: "info" + createdAt: "2026-03-30T09:12:00Z" + - id: "rbac-002" + date: "2026-03-29T18:40:00Z" + changeType: "PermissionRemoved" + source: "azure-entra" + createdBy: "security-automation" + configId: "cfg-sql-001" + configName: "prod-sql-primary" + configType: "MSSQL::Database" + permission: + user: "contractor-temp" + role: "db_datareader" + description: "PermissionRemoved: user contractor-temp, role db_datareader" + status: "info" + createdAt: "2026-03-29T18:40:00Z" + - id: "rbac-004" + date: "2026-03-28T13:05:00Z" + changeType: "PermissionGranted" + source: "okta" + createdBy: "bob@flanksource.com" + configId: "cfg-analytics-001" + configName: "analytics-db" + configType: "MSSQL::Database" + description: "Granted db_ddladmin to deploy-bot on analytics-db" + status: "info" + createdAt: "2026-03-28T13:05:00Z" + - id: "rbac-005" + date: "2026-03-27T07:20:00Z" + changeType: "PermissionAdded" + source: "okta" + configId: "cfg-keyvault-001" + configName: "prod-keyvault" + configType: "Azure::KeyVault" + permission: + group: "break-glass-admins" + role: "Secrets Officer" + description: "PermissionAdded: role Secrets Officer, group break-glass-admins" + status: "info" + createdAt: "2026-03-27T07:20:00Z" + + - type: "changes" + title: "Backup Activity" + changes: + - id: "bak-001" + date: "2026-03-30T02:00:00Z" + changeType: "BackupStarted" + source: "aws-backup" + description: "Nightly snapshot started for incident-commander-db" + status: "info" + createdAt: "2026-03-30T02:00:00Z" + - id: "bak-002" + date: "2026-03-30T02:08:00Z" + changeType: "BackupCompleted" + source: "aws-backup" + description: "Nightly snapshot completed for incident-commander-db (4.3 GB)" + status: "info" + createdAt: "2026-03-30T02:08:00Z" + - id: "bak-003" + date: "2026-03-29T02:01:00Z" + changeType: "BackupFailed" + source: "aws-backup" + description: "Snapshot failed for incident-commander-db after storage timeout" + status: "high" + createdAt: "2026-03-29T02:01:00Z" + + - type: "changes" + title: "Deployment Changes" + changes: + - id: "dep-001" + date: "2026-03-30T08:15:00Z" + changeType: "diff" + source: "argocd" + createdBy: "deploy-bot" + description: "Deployment incident-commander image updated: v1.4.199 -> v1.4.200" + status: "low" + createdAt: "2026-03-30T08:15:00Z" + - id: "dep-002" + date: "2026-03-30T07:30:00Z" + changeType: "Pulled" + source: "kubernetes" + description: "Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42" + status: "info" + createdAt: "2026-03-30T07:30:00Z" + - id: "dep-003" + date: "2026-03-29T22:00:00Z" + changeType: "ScalingReplicaSet" + source: "kubernetes" + createdBy: "cluster-autoscaler" + description: "Deployment incident-commander scaled from 2 to 4 replicas" + status: "low" + createdAt: "2026-03-29T22:00:00Z" diff --git a/report/types.ts b/report/types.ts index 1bd9a1f1e..03e874b10 100644 --- a/report/types.ts +++ b/report/types.ts @@ -127,12 +127,22 @@ export interface ApplicationViewData { columnOptions?: Record; } +export interface ApplicationPermissionChange { + user?: string; + role?: string; + group?: string; +} + export interface ApplicationChange { id: string; date: string; changeType?: string; source?: string; createdBy?: string; + configId?: string; + configName?: string; + configType?: string; + permission?: ApplicationPermissionChange; description: string; status: string; createdAt: string; From d065da74d89f16d9e4c9d751ff4e5396ccc1694c Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 7 Apr 2026 15:55:56 +0300 Subject: [PATCH 32/48] feat(catalog_report): add settings, audit trail, and change categorization to catalog reports Introduces configurable report settings (filters, thresholds, category mappings) loaded from YAML, an audit page showing build metadata and query performance, and automatic categorization of config changes for specialized rendering of RBAC, backup, and deployment events. The Export API now returns detailed render metadata and supports build commit tracking. --- api/catalog_report.go | 40 ++++ api/global.go | 1 + catalog_report/default-settings.yaml | 42 ++++ catalog_report/export.go | 118 +++++++++- catalog_report/render_facet.go | 31 +-- catalog_report/report.go | 77 +++++-- catalog_report/settings.go | 130 +++++++++++ catalog_report/settings_test.go | 141 ++++++++++++ cmd/catalog_report.go | 57 ++++- main.go | 1 + report/CatalogReport.tsx | 55 ++++- report/catalog-report-types.ts | 35 +++ report/components/AuditPage.tsx | 120 ++++++++++ report/components/BackupChanges.tsx | 85 ++++--- report/components/BackupsSection.tsx | 77 +++++-- report/components/ConfigChangesSection.tsx | 2 +- report/components/ConfigInsightsSection.tsx | 4 +- report/components/ConfigTreeSection.tsx | 35 ++- report/components/DeploymentChanges.tsx | 147 +++++++----- report/components/DynamicSection.tsx | 2 +- report/components/GitRef.tsx | 43 ++++ report/components/RBACChanges.tsx | 199 +++++++++-------- report/components/ScraperCard.tsx | 84 +++---- report/components/change-section-utils.ts | 233 +++++++++++++++++--- report/config-types.ts | 1 + report/facet.go | 50 ++++- report/kitchen-sink/ChangesPage.tsx | 29 ++- report/kitchen-sink/DynamicSectionsPage.tsx | 2 +- report/testdata/kitchen-sink.yaml | 107 +++++++++ report/types.ts | 1 + 30 files changed, 1577 insertions(+), 372 deletions(-) create mode 100644 catalog_report/default-settings.yaml create mode 100644 catalog_report/settings.go create mode 100644 catalog_report/settings_test.go create mode 100644 report/components/AuditPage.tsx create mode 100644 report/components/GitRef.tsx diff --git a/api/catalog_report.go b/api/catalog_report.go index 54a1c88ce..9666456e5 100644 --- a/api/catalog_report.go +++ b/api/catalog_report.go @@ -14,6 +14,41 @@ func ConfigPermalink(configID string) string { return fmt.Sprintf("%s/catalog/%s", FrontendURL, configID) } +type CatalogReportThresholds struct { + StaleDays int `json:"staleDays"` + ReviewOverdueDays int `json:"reviewOverdueDays"` +} + +type CatalogReportOptions struct { + Title string `json:"title"` + Since string `json:"since"` + Sections CatalogReportSections `json:"sections"` + Recursive bool `json:"recursive"` + GroupBy string `json:"groupBy"` + ChangeArtifacts bool `json:"changeArtifacts"` + Filters []string `json:"filters,omitempty"` + Thresholds *CatalogReportThresholds `json:"thresholds,omitempty"` + CategoryMappings map[string][]string `json:"categoryMappings,omitempty"` +} + +type CatalogReportAudit struct { + BuildCommit string `json:"buildCommit"` + BuildVersion string `json:"buildVersion"` + GitStatus string `json:"gitStatus,omitempty"` + Options CatalogReportOptions `json:"options"` + Scrapers []ScraperInfo `json:"scrapers"` + Queries []CatalogReportQuery `json:"queries"` +} + +type CatalogReportQuery struct { + Name string `json:"name"` + Args string `json:"args,omitempty"` + Count int `json:"count"` + Duration int64 `json:"duration"` + Error string `json:"error,omitempty"` + Summary string `json:"summary,omitempty"` +} + type CatalogReport struct { Title string `json:"title"` GeneratedAt time.Time `json:"generatedAt"` @@ -25,6 +60,10 @@ type CatalogReport struct { GroupBy string `json:"groupBy,omitempty"` Entries []CatalogReportEntry `json:"entries"` + CategoryMappings map[string][]string `json:"categoryMappings,omitempty"` + Thresholds *CatalogReportThresholds `json:"thresholds,omitempty"` + Audit *CatalogReportAudit `json:"audit,omitempty"` + // Deprecated: use Entries[0] for single-config reports ConfigItem models.ConfigItem `json:"configItem"` Parents []models.ConfigItem `json:"parents"` @@ -91,6 +130,7 @@ type CatalogReportChange struct { Severity string `json:"severity,omitempty"` Source string `json:"source,omitempty"` Summary string `json:"summary,omitempty"` + Details map[string]any `json:"details,omitempty"` CreatedBy string `json:"createdBy,omitempty"` ExternalCreatedBy string `json:"externalCreatedBy,omitempty"` CreatedAt string `json:"createdAt,omitempty"` diff --git a/api/global.go b/api/global.go index 7dd480f16..3eb8a3d25 100644 --- a/api/global.go +++ b/api/global.go @@ -6,6 +6,7 @@ import ( var ( BuildVersion string + BuildCommit string SystemUserID *uuid.UUID CanaryCheckerPath string diff --git a/catalog_report/default-settings.yaml b/catalog_report/default-settings.yaml new file mode 100644 index 000000000..1f2e3c6c8 --- /dev/null +++ b/catalog_report/default-settings.yaml @@ -0,0 +1,42 @@ +filters: + - "type!=Kubernetes::ConfigMap" + - "type!=Kubernetes::Secret" + - "type!=Kubernetes::Event" + +thresholds: + staleDays: 90 + reviewOverdueDays: 90 + +categoryMappings: + rbac.granted: + - PermissionGranted + - PermissionAdded + - IAMRoleAdded + rbac.revoked: + - PermissionRevoked + - PermissionRemoved + - IAMRoleRemoved + backup.success: + - BackupCompleted + - BackupSuccessful + - BACKUP_DB@low + backup.failed: + - BackupFailed + - BACKUP_DB@high + backup.progress: + - BackupStarted + - BackupRunning + - BackupEnqueued + backup.restore: + - BackupRestored + - RestoreCompleted + deployment.spec: + - diff + deployment.success: + - CodeDeployment@info + deployment.failed: + - CodeDeployment@failed + deployment.scale: + - ScalingReplicaSet + deployment.policy: + - PolicyUpdate diff --git a/catalog_report/export.go b/catalog_report/export.go index 3c6c05b89..fd025700b 100644 --- a/catalog_report/export.go +++ b/catalog_report/export.go @@ -2,30 +2,128 @@ package catalog_report import ( "encoding/json" + "os/exec" + "sort" + "strings" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query" + "github.com/google/uuid" "github.com/flanksource/incident-commander/api" + "github.com/flanksource/incident-commander/scraper_report" ) -func Export(ctx context.Context, configs []models.ConfigItem, opts Options, format string) ([]byte, error) { - report, err := BuildReport(ctx, configs, opts) +type ExportResult struct { + Data []byte + SrcDir string + Entry string + DataFile string + Settings string +} + +func Export(ctx context.Context, configs []models.ConfigItem, opts Options, format string) (*ExportResult, error) { + var queryLog *query.QueryLog + if opts.Audit { + ctx, queryLog = query.WithQueryLog(ctx) + } + + r, scraperIDs, err := BuildReport(ctx, configs, opts) if err != nil { return nil, err } ctx.Logger.V(3).Infof("Report built: %d entries, %d changes, %d analyses", - len(report.Entries), len(report.Changes), len(report.Analyses)) + len(r.Entries), len(r.Changes), len(r.Analyses)) + + if opts.Audit { + r.Audit = buildAudit(ctx, opts, scraperIDs, queryLog) + } + + result := &ExportResult{} + if opts.Settings != nil { + result.Settings = opts.SettingsPath + } switch format { case "html", "facet-html": - return RenderFacetHTML(ctx, report) + result.Data, result.SrcDir, result.Entry, result.DataFile, err = renderFacetResult(ctx, r, "html") case "pdf", "facet-pdf": - return RenderFacetPDF(ctx, report) + result.Data, result.SrcDir, result.Entry, result.DataFile, err = renderFacetResult(ctx, r, "pdf") default: - return json.MarshalIndent(report, "", " ") + result.Data, err = json.MarshalIndent(r, "", " ") } + + return result, err +} + +func buildAudit(ctx context.Context, opts Options, scraperIDs []string, queryLog *query.QueryLog) *api.CatalogReportAudit { + audit := &api.CatalogReportAudit{ + BuildCommit: api.BuildCommit, + BuildVersion: api.BuildVersion, + Options: api.CatalogReportOptions{ + Title: opts.Title, + Since: opts.Since.String(), + Sections: opts.Sections, + Recursive: opts.Recursive, + GroupBy: opts.GroupBy, + ChangeArtifacts: opts.ChangeArtifacts, + }, + Scrapers: []api.ScraperInfo{}, + Queries: []api.CatalogReportQuery{}, + } + + if opts.Settings != nil { + audit.Options.Filters = opts.Settings.Filters + if opts.Settings.Thresholds.StaleDays > 0 || opts.Settings.Thresholds.ReviewOverdueDays > 0 { + audit.Options.Thresholds = &api.CatalogReportThresholds{ + StaleDays: opts.StaleDays(), + ReviewOverdueDays: opts.ReviewOverdueDays(), + } + } + audit.Options.CategoryMappings = opts.Settings.CategoryMappings + } + + audit.GitStatus = gitStatus() + + if queryLog != nil { + for _, e := range queryLog.Entries() { + audit.Queries = append(audit.Queries, api.CatalogReportQuery{ + Name: e.Name, + Args: e.Args, + Count: e.Count, + Duration: e.Duration, + Error: e.Error, + Summary: e.Summary, + Pretty: e.Pretty, + }) + } + } + + sort.Strings(scraperIDs) + for _, sid := range scraperIDs { + id, err := uuid.Parse(sid) + if err != nil { + continue + } + info, err := scraper_report.BuildScraperInfo(ctx, id) + if err != nil { + ctx.Logger.V(2).Infof("failed to build scraper info for %s: %v", sid, err) + continue + } + audit.Scrapers = append(audit.Scrapers, *info) + } + + return audit +} + +func gitStatus() string { + out, err := exec.Command("git", "status", "--short").Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) } func initSlices(r *api.CatalogReport) api.CatalogReport { @@ -88,5 +186,13 @@ func initSlices(r *api.CatalogReport) api.CatalogReport { out.ConfigGroups[i].AccessLogs = []api.CatalogReportAccessLog{} } } + if out.Audit != nil { + if out.Audit.Scrapers == nil { + out.Audit.Scrapers = []api.ScraperInfo{} + } + if out.Audit.Queries == nil { + out.Audit.Queries = []api.CatalogReportQuery{} + } + } return out } diff --git a/catalog_report/render_facet.go b/catalog_report/render_facet.go index b4dcc4d6d..9e3000bdc 100644 --- a/catalog_report/render_facet.go +++ b/catalog_report/render_facet.go @@ -9,27 +9,28 @@ import ( "github.com/flanksource/incident-commander/report" ) -func RenderFacetHTML(ctx context.Context, r *api.CatalogReport) ([]byte, error) { - return renderWithFacet(ctx, r, "html") -} - -func RenderFacetPDF(ctx context.Context, r *api.CatalogReport) ([]byte, error) { - return renderWithFacet(ctx, r, "pdf") -} - -func renderWithFacet(ctx context.Context, r *api.CatalogReport, format string) ([]byte, error) { +func renderFacetResult(ctx context.Context, r *api.CatalogReport, format string) (data []byte, srcDir, entry, dataFile string, err error) { if r == nil { - return nil, fmt.Errorf("catalog report must not be nil") + return nil, "", "", "", fmt.Errorf("catalog report must not be nil") } ctx.Logger.V(3).Infof("Rendering catalog facet-%s", format) - data := initSlices(r) - result, err := report.RenderCLI(data, format, "CatalogReport.tsx") + result, err := report.RenderCLI(initSlices(r), format, "CatalogReport.tsx") if err != nil { - return nil, err + return nil, "", "", "", err } - ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result)/1024, format) - return result, nil + ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result.Data)/1024, format) + return result.Data, result.SrcDir, result.Entry, result.DataFile, nil +} + +func RenderFacetHTML(ctx context.Context, r *api.CatalogReport) ([]byte, error) { + data, _, _, _, err := renderFacetResult(ctx, r, "html") + return data, err +} + +func RenderFacetPDF(ctx context.Context, r *api.CatalogReport) ([]byte, error) { + data, _, _, _, err := renderFacetResult(ctx, r, "pdf") + return data, err } diff --git a/catalog_report/report.go b/catalog_report/report.go index 57dac27c1..1c4ef8087 100644 --- a/catalog_report/report.go +++ b/catalog_report/report.go @@ -24,6 +24,23 @@ type Options struct { Recursive bool GroupBy string // "merged" (default) or "config" ChangeArtifacts bool + Audit bool + Settings *Settings + SettingsPath string +} + +func (o Options) StaleDays() int { + if o.Settings != nil && o.Settings.Thresholds.StaleDays > 0 { + return o.Settings.Thresholds.StaleDays + } + return 90 +} + +func (o Options) ReviewOverdueDays() int { + if o.Settings != nil && o.Settings.Thresholds.ReviewOverdueDays > 0 { + return o.Settings.Thresholds.ReviewOverdueDays + } + return 90 } func (o Options) WithDefaults() Options { @@ -39,9 +56,9 @@ func (o Options) WithDefaults() Options { return o } -func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) (*api.CatalogReport, error) { +func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) (*api.CatalogReport, []string, error) { if len(configs) == 0 { - return nil, fmt.Errorf("no config items provided") + return nil, nil, fmt.Errorf("no config items provided") } opts = opts.WithDefaults() sinceTime := time.Now().Add(-opts.Since) @@ -59,10 +76,17 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) report.Parents = resolveParents(ctx, &configs[0]) + scraperIDSet := make(map[string]bool) + for _, config := range configs { + if config.ScraperID != nil && *config.ScraperID != "" { + scraperIDSet[*config.ScraperID] = true + } + } + for _, config := range configs { - entry, err := buildEntry(ctx, &config, opts, sinceTime) + entry, entryScraperIDs, err := buildEntry(ctx, &config, opts, sinceTime) if err != nil { - return nil, fmt.Errorf("failed to build entry for %s: %w", config.GetName(), err) + return nil, nil, fmt.Errorf("failed to build entry for %s: %w", config.GetName(), err) } report.Entries = append(report.Entries, *entry) @@ -70,6 +94,10 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) report.Analyses = append(report.Analyses, entry.Analyses...) report.Access = append(report.Access, entry.Access...) report.AccessLogs = append(report.AccessLogs, entry.AccessLogs...) + + for _, id := range entryScraperIDs { + scraperIDSet[id] = true + } } if opts.Sections.ConfigJSON && configs[0].Config != nil { @@ -83,10 +111,25 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) report.AccessLogs = nil } - return report, nil + if opts.Settings != nil { + if len(opts.Settings.CategoryMappings) > 0 { + report.CategoryMappings = opts.Settings.CategoryMappings + } + report.Thresholds = &api.CatalogReportThresholds{ + StaleDays: opts.StaleDays(), + ReviewOverdueDays: opts.ReviewOverdueDays(), + } + } + + var scraperIDs []string + for id := range scraperIDSet { + scraperIDs = append(scraperIDs, id) + } + + return report, scraperIDs, nil } -func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time) (*api.CatalogReportEntry, error) { +func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time) (*api.CatalogReportEntry, []string, error) { entry := &api.CatalogReportEntry{ ConfigItem: api.NewCatalogReportConfigItem(*config), } @@ -98,17 +141,21 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si tree, err := query.ConfigTree(ctx, config.ID, query.ConfigTreeOptions{}) if err != nil { - return nil, fmt.Errorf("failed to build config tree: %w", err) + return nil, nil, fmt.Errorf("failed to build config tree: %w", err) } targetIDs := tree.OutgoingIDs() configMap := make(map[uuid.UUID]models.ConfigItem) items, err := query.GetConfigsByIDs(ctx, targetIDs) if err != nil { - return nil, fmt.Errorf("failed to load config items: %w", err) + return nil, nil, fmt.Errorf("failed to load config items: %w", err) } + var scraperIDs []string for _, ci := range items { configMap[ci.ID] = ci + if ci.ScraperID != nil && *ci.ScraperID != "" { + scraperIDs = append(scraperIDs, *ci.ScraperID) + } } configMeta := func(configID string) (string, string) { @@ -135,7 +182,7 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si }, }) if err != nil { - return nil, fmt.Errorf("failed to get changes: %w", err) + return nil, nil, fmt.Errorf("failed to get changes: %w", err) } entry.Changes = lo.Map(resp.Changes, func(c query.ConfigChangeRow, _ int) api.CatalogReportChange { name, typ := configMeta(c.ConfigID) @@ -175,7 +222,7 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si }, }) if err != nil { - return nil, fmt.Errorf("failed to get insights: %w", err) + return nil, nil, fmt.Errorf("failed to get insights: %w", err) } entry.Analyses = lo.Map(resp.Insights, func(a models.ConfigAnalysis, _ int) api.CatalogReportAnalysis { name, typ := configMeta(a.ConfigID.String()) @@ -187,7 +234,7 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si if opts.Sections.Access { rbacRows, err := db.GetRBACAccessByConfigIDs(ctx, targetIDs) if err != nil { - return nil, fmt.Errorf("failed to get access: %w", err) + return nil, nil, fmt.Errorf("failed to get access: %w", err) } entry.RBACResources = groupRBACByConfig(rbacRows, configMap, opts) for _, r := range entry.RBACResources { @@ -198,7 +245,7 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si if opts.Sections.AccessLogs { logs, err := getAccessLogs(ctx, targetIDs, sinceTime) if err != nil { - return nil, fmt.Errorf("failed to get access logs: %w", err) + return nil, nil, fmt.Errorf("failed to get access logs: %w", err) } entry.AccessLogs = lo.Map(logs, func(l accessLogRow, _ int) api.CatalogReportAccessLog { return newAccessLogEntry(l) @@ -209,7 +256,7 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si entry.RelationshipTree = configTreeNodeToReport(tree) } - return entry, nil + return entry, scraperIDs, nil } func buildConfigGroups(report *api.CatalogReport, configMap map[uuid.UUID]models.ConfigItem) []api.CatalogReportConfigGroup { @@ -398,8 +445,8 @@ func isEmbeddableContentType(ct string) bool { } func groupRBACByConfig(rows []db.RBACAccessRow, configMap map[uuid.UUID]models.ConfigItem, opts Options) []api.RBACResource { - staleThreshold := time.Now().AddDate(0, 0, -90) - reviewThreshold := time.Now().AddDate(0, 0, -90) + staleThreshold := time.Now().AddDate(0, 0, -opts.StaleDays()) + reviewThreshold := time.Now().AddDate(0, 0, -opts.ReviewOverdueDays()) grouped := make(map[uuid.UUID]*api.RBACResource) var order []uuid.UUID diff --git a/catalog_report/settings.go b/catalog_report/settings.go new file mode 100644 index 000000000..b88a8542d --- /dev/null +++ b/catalog_report/settings.go @@ -0,0 +1,130 @@ +package catalog_report + +import ( + _ "embed" + "fmt" + "os" + "sort" + "strings" + + "github.com/flanksource/clicky/api" + "sigs.k8s.io/yaml" +) + +const EmbeddedSettingsSource = "embedded defaults" + +//go:embed default-settings.yaml +var defaultSettingsYAML []byte + +type Settings struct { + Filters []string `json:"filters,omitempty" yaml:"filters,omitempty"` + Thresholds SettingsThresholds `json:"thresholds,omitempty" yaml:"thresholds,omitempty"` + CategoryMappings map[string][]string `json:"categoryMappings,omitempty" yaml:"categoryMappings,omitempty"` +} + +type SettingsThresholds struct { + StaleDays int `json:"staleDays,omitempty" yaml:"staleDays,omitempty"` + ReviewOverdueDays int `json:"reviewOverdueDays,omitempty" yaml:"reviewOverdueDays,omitempty"` +} + +func (s *Settings) Clone() *Settings { + if s == nil { + return &Settings{} + } + + out := &Settings{ + Thresholds: s.Thresholds, + } + + if len(s.Filters) > 0 { + out.Filters = append([]string(nil), s.Filters...) + } + + if len(s.CategoryMappings) > 0 { + out.CategoryMappings = make(map[string][]string, len(s.CategoryMappings)) + for key, values := range s.CategoryMappings { + out.CategoryMappings[key] = append([]string(nil), values...) + } + } + + return out +} + +func parseSettings(data []byte, source string, base *Settings) (*Settings, error) { + settings := base.Clone() + if err := yaml.Unmarshal(data, settings); err != nil { + return nil, fmt.Errorf("failed to parse settings %s: %w", source, err) + } + return settings, nil +} + +func LoadDefaultSettings() (*Settings, error) { + return parseSettings(defaultSettingsYAML, EmbeddedSettingsSource, nil) +} + +func LoadSettings(path string) (*Settings, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read settings file %s: %w", path, err) + } + return parseSettings(data, path, nil) +} + +func ResolveSettings(path string) (*Settings, string, error) { + defaults, err := LoadDefaultSettings() + if err != nil { + return nil, "", err + } + + if path == "" { + return defaults, EmbeddedSettingsSource, nil + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, "", fmt.Errorf("failed to read settings file %s: %w", path, err) + } + + settings, err := parseSettings(data, path, defaults) + if err != nil { + return nil, "", err + } + + return settings, fmt.Sprintf("%s + %s", EmbeddedSettingsSource, path), nil +} + +func (s *Settings) Pretty() api.Text { + if s == nil { + return api.Text{Content: "", Style: "text-gray-500"} + } + items := []api.KeyValuePair{} + if len(s.Filters) > 0 { + items = append(items, api.KeyValue("Filters", strings.Join(s.Filters, ", "))) + } + if s.Thresholds.StaleDays > 0 || s.Thresholds.ReviewOverdueDays > 0 { + items = append(items, api.KeyValue("Stale", fmt.Sprintf("%dd", s.Thresholds.StaleDays))) + items = append(items, api.KeyValue("Review Overdue", fmt.Sprintf("%dd", s.Thresholds.ReviewOverdueDays))) + } + if len(s.CategoryMappings) > 0 { + keys := make([]string, 0, len(s.CategoryMappings)) + for k := range s.CategoryMappings { + keys = append(keys, k) + } + sort.Strings(keys) + var cats []string + for _, k := range keys { + cats = append(cats, fmt.Sprintf("%s: %s", k, strings.Join(s.CategoryMappings[k], ", "))) + } + items = append(items, api.KeyValue("Categories", strings.Join(cats, " | "))) + } + return api.Text{}.Add(api.DescriptionList{Items: items}) +} + +// FilterQuery returns the filters as a single search query string +// that can be appended to the ResourceSelector search. +func (s *Settings) FilterQuery() string { + if s == nil || len(s.Filters) == 0 { + return "" + } + return strings.Join(s.Filters, " ") +} diff --git a/catalog_report/settings_test.go b/catalog_report/settings_test.go new file mode 100644 index 000000000..e1528216e --- /dev/null +++ b/catalog_report/settings_test.go @@ -0,0 +1,141 @@ +package catalog_report + +import ( + "os" + "path/filepath" + "strings" + + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe("Settings", func() { + ginkgo.Describe("LoadSettings", func() { + ginkgo.It("parses valid YAML", func() { + content := ` +filters: + - "type!=Kubernetes::ConfigMap" + - "type!=Kubernetes::Secret" +thresholds: + staleDays: 60 + reviewOverdueDays: 30 +categoryMappings: + rbac.granted: + - PermissionGranted + - PermissionAdded + backup.failed: + - BackupFailed +` + path := filepath.Join(os.TempDir(), "test-settings.yaml") + Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) + defer os.Remove(path) + + s, err := LoadSettings(path) + Expect(err).ToNot(HaveOccurred()) + Expect(s.Filters).To(Equal([]string{"type!=Kubernetes::ConfigMap", "type!=Kubernetes::Secret"})) + Expect(s.Thresholds.StaleDays).To(Equal(60)) + Expect(s.Thresholds.ReviewOverdueDays).To(Equal(30)) + Expect(s.CategoryMappings).To(HaveKey("rbac.granted")) + Expect(s.CategoryMappings["rbac.granted"]).To(Equal([]string{"PermissionGranted", "PermissionAdded"})) + Expect(s.CategoryMappings["backup.failed"]).To(Equal([]string{"BackupFailed"})) + }) + + ginkgo.It("returns error for missing file", func() { + _, err := LoadSettings("/nonexistent/path.yaml") + Expect(err).To(HaveOccurred()) + }) + }) + + ginkgo.Describe("LoadDefaultSettings", func() { + ginkgo.It("loads embedded defaults", func() { + s, err := LoadDefaultSettings() + Expect(err).ToNot(HaveOccurred()) + Expect(s.Filters).To(ContainElement("type!=Kubernetes::ConfigMap")) + Expect(s.Thresholds.StaleDays).To(Equal(90)) + Expect(s.Thresholds.ReviewOverdueDays).To(Equal(90)) + Expect(s.CategoryMappings).To(HaveKey("rbac.granted")) + Expect(s.CategoryMappings["rbac.granted"]).To(ContainElement("PermissionAdded")) + Expect(s.CategoryMappings["backup.failed"]).To(ContainElement("BACKUP_DB@high")) + Expect(s.CategoryMappings["deployment.failed"]).To(ContainElement("CodeDeployment@failed")) + }) + }) + + ginkgo.Describe("ResolveSettings", func() { + ginkgo.It("uses embedded defaults when no path is provided", func() { + s, source, err := ResolveSettings("") + Expect(err).ToNot(HaveOccurred()) + Expect(source).To(Equal(EmbeddedSettingsSource)) + Expect(s.Filters).To(ContainElement("type!=Kubernetes::Secret")) + Expect(s.CategoryMappings).To(HaveKey("backup.failed")) + }) + + ginkgo.It("overlays file settings on top of embedded defaults", func() { + content := ` +filters: + - "name=test" +thresholds: + staleDays: 60 +categoryMappings: + backup.failed: + - BACKUP_DB@high + deployment.failed: + - CodeDeployment@failed +` + path := filepath.Join(os.TempDir(), "overlay-settings.yaml") + Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) + defer os.Remove(path) + + s, source, err := ResolveSettings(path) + Expect(err).ToNot(HaveOccurred()) + Expect(strings.Contains(source, EmbeddedSettingsSource)).To(BeTrue()) + Expect(strings.Contains(source, path)).To(BeTrue()) + Expect(s.Filters).To(Equal([]string{"name=test"})) + Expect(s.Thresholds.StaleDays).To(Equal(60)) + Expect(s.Thresholds.ReviewOverdueDays).To(Equal(90)) + Expect(s.CategoryMappings["backup.failed"]).To(Equal([]string{"BACKUP_DB@high"})) + Expect(s.CategoryMappings["deployment.failed"]).To(Equal([]string{"CodeDeployment@failed"})) + Expect(s.CategoryMappings).To(HaveKey("rbac.granted")) + }) + }) + + ginkgo.Describe("FilterQuery", func() { + ginkgo.It("joins filters into search string", func() { + s := &Settings{Filters: []string{"type!=Kubernetes::ConfigMap", "type!=Kubernetes::Secret"}} + Expect(s.FilterQuery()).To(Equal("type!=Kubernetes::ConfigMap type!=Kubernetes::Secret")) + }) + + ginkgo.It("returns empty for nil settings", func() { + var s *Settings + Expect(s.FilterQuery()).To(Equal("")) + }) + + ginkgo.It("returns empty for no filters", func() { + s := &Settings{} + Expect(s.FilterQuery()).To(Equal("")) + }) + }) + + ginkgo.Describe("Options threshold methods", func() { + ginkgo.It("returns defaults when no settings", func() { + opts := Options{} + Expect(opts.StaleDays()).To(Equal(90)) + Expect(opts.ReviewOverdueDays()).To(Equal(90)) + }) + + ginkgo.It("returns settings values", func() { + opts := Options{ + Settings: &Settings{ + Thresholds: SettingsThresholds{StaleDays: 60, ReviewOverdueDays: 30}, + }, + } + Expect(opts.StaleDays()).To(Equal(60)) + Expect(opts.ReviewOverdueDays()).To(Equal(30)) + }) + + ginkgo.It("returns defaults when settings thresholds are zero", func() { + opts := Options{Settings: &Settings{}} + Expect(opts.StaleDays()).To(Equal(90)) + Expect(opts.ReviewOverdueDays()).To(Equal(90)) + }) + }) +}) diff --git a/cmd/catalog_report.go b/cmd/catalog_report.go index 91e647933..ca6d46eee 100644 --- a/cmd/catalog_report.go +++ b/cmd/catalog_report.go @@ -18,10 +18,11 @@ import ( ) var ( - catalogReportFormat string - catalogReportOutFile string - catalogReportSince string - catalogReportTitle string + catalogReportFormat string + catalogReportOutFile string + catalogReportSince string + catalogReportTitle string + catalogReportSettings string catalogReportChanges bool catalogReportInsights bool @@ -32,6 +33,7 @@ var ( catalogReportRecursive bool catalogReportGroupBy string catalogReportChangeArtifacts bool + catalogReportAudit bool ) var CatalogReportCmd = &cobra.Command{ @@ -67,14 +69,21 @@ Examples: shutdown.AddHookWithPriority("database", shutdown.PriorityCritical, stop) shutdown.WaitForSignal() - configs, err := resolveConfigs(ctx, args, 0) + opts := buildCatalogReportOptions() + + queryArgs := args + if opts.Settings != nil { + if fq := opts.Settings.FilterQuery(); fq != "" { + queryArgs = append(queryArgs, fq) + } + } + + configs, err := resolveConfigs(ctx, queryArgs, 0) if err != nil { return err } - opts := buildCatalogReportOptions() - - data, err := catalog_report.Export(ctx, configs, opts, catalogReportFormat) + result, err := catalog_report.Export(ctx, configs, opts, catalogReportFormat) if err != nil { shutdown.ShutdownAndExit(1, err.Error()) return err @@ -84,14 +93,28 @@ Examples: if out == "" { out = "stdout" } - logger.Infof("Rendering catalog report to %s (%s) %dKB", out, catalogReportFormat, len(data)/1024) + + details := fmt.Sprintf("Rendering catalog report to %s (%s) %dKB", out, catalogReportFormat, len(result.Data)/1024) + if opts.Settings != nil { + details += fmt.Sprintf(" settings=%s\n%s", result.Settings, opts.Settings.Pretty().ANSI()) + } + if result.SrcDir != "" { + details += fmt.Sprintf(" dir=%s", result.SrcDir) + } + if result.Entry != "" { + details += fmt.Sprintf(" entry=%s", result.Entry) + } + if result.DataFile != "" { + details += fmt.Sprintf(" data=%s", result.DataFile) + } + logger.Infof(details) if catalogReportOutFile != "" { - if err := os.WriteFile(catalogReportOutFile, data, 0600); err != nil { + if err := os.WriteFile(catalogReportOutFile, result.Data, 0600); err != nil { return fmt.Errorf("failed to write output file: %w", err) } } else { - fmt.Print(string(data)) + fmt.Print(string(result.Data)) } return nil @@ -104,6 +127,7 @@ func buildCatalogReportOptions() catalog_report.Options { Recursive: catalogReportRecursive, GroupBy: catalogReportGroupBy, ChangeArtifacts: catalogReportChangeArtifacts, + Audit: catalogReportAudit, Sections: api.CatalogReportSections{ Changes: catalogReportChanges, Insights: catalogReportInsights, @@ -120,6 +144,15 @@ func buildCatalogReportOptions() catalog_report.Options { } } + if catalogReportSettings != "" { + settings, err := catalog_report.LoadSettings(catalogReportSettings) + if err != nil { + logger.Fatalf("failed to load settings: %v", err) + } + opts.Settings = settings + opts.SettingsPath = catalogReportSettings + } + return opts } @@ -139,6 +172,8 @@ func init() { CatalogReportCmd.Flags().BoolVar(&catalogReportAccess, "access", true, "Include RBAC access section") CatalogReportCmd.Flags().BoolVar(&catalogReportAccessLogs, "access-logs", true, "Include access logs section") CatalogReportCmd.Flags().BoolVar(&catalogReportConfigJSON, "config-json", false, "Include raw config JSON") + CatalogReportCmd.Flags().StringVar(&catalogReportSettings, "settings", "", "Path to report settings YAML file") + CatalogReportCmd.Flags().BoolVar(&catalogReportAudit, "audit", false, "Append an audit page with settings, build info, queries, and scraper provenance") Catalog.AddCommand(CatalogReportCmd) } diff --git a/main.go b/main.go index 5b7b56d5c..7a2301e40 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ func main() { } api.BuildVersion = version + api.BuildCommit = commit cmd.Root.AddCommand(&cobra.Command{ Use: "version", diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index 773abc901..093c2096f 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -2,14 +2,20 @@ import React from 'react'; import { Document, Page, Header, Footer, Section } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { CatalogReportData, CatalogReportConfigGroup } from './catalog-report-types.ts'; +import type { ConfigChange } from './config-types.ts'; import ConfigChangesSection from './components/ConfigChangesSection.tsx'; import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; +import RBACChanges from './components/RBACChanges.tsx'; +import BackupChanges from './components/BackupChanges.tsx'; +import DeploymentChanges from './components/DeploymentChanges.tsx'; +import { categorizeChanges, configChangeToApplicationChange } from './components/change-section-utils.ts'; import ConfigRelationshipGraph from './components/ConfigRelationshipGraph.tsx'; import ConfigTreeSection from './components/ConfigTreeSection.tsx'; import CatalogAccessSection from './components/CatalogAccessSection.tsx'; import CatalogAccessLogsSection from './components/CatalogAccessLogsSection.tsx'; import RBACMatrixSection from './components/RBACMatrixSection.tsx'; import ArtifactAppendix from './components/ArtifactAppendix.tsx'; +import AuditPage from './components/AuditPage.tsx'; import CoverPage from './components/CoverPage.tsx'; import CatalogList from './components/CatalogList.tsx'; import PageHeader from './components/PageHeader.tsx'; @@ -40,6 +46,12 @@ function CatalogCoverPage({ data }: { data: CatalogReportData }) { {data.groupBy === 'config' && ` · Grouped by config (${(data.configGroups || []).length} items)`}
)} + {data.thresholds && ( +
+ Stale access: {data.thresholds.staleDays}d + Review overdue: {data.thresholds.reviewOverdueDays}d +
+ )} ); } @@ -59,6 +71,37 @@ function ConfigJSONSection({ json }: { json: string }) { ); } +function CategorizedChangesSection({ changes, categoryMappings, hideConfigName }: { + changes?: ConfigChange[]; + categoryMappings?: Record; + hideConfigName?: boolean; +}) { + if (!changes?.length) return null; + const { rbac, backup, deployment, uncategorized } = categorizeChanges(changes, categoryMappings); + return ( + <> + {rbac.length > 0 && ( +
+ configChangeToApplicationChange(change, category))} /> +
+ )} + {backup.length > 0 && ( +
+ configChangeToApplicationChange(change, category))} /> +
+ )} + {deployment.length > 0 && ( +
+ configChangeToApplicationChange(change, category))} /> +
+ )} + {uncategorized.length > 0 && ( + + )} + + ); +} + function ConfigGroupHeader({ group }: { group: CatalogReportConfigGroup }) { const ci = group.configItem; return ( @@ -111,7 +154,7 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { {data.groupBy === 'config' && (data.entries || []).map((entry, idx) => ( - + {(entry.rbacResources || []).map((resource, rIdx) => ( @@ -121,7 +164,7 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { {data.groupBy !== 'config' && ( <> - + )} @@ -141,7 +184,7 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { {data.groupBy === 'config' && (data.configGroups || []).map((group, idx) => ( - + @@ -152,6 +195,12 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { e.changes || [])} /> + + {data.audit && ( + + + + )} ); } diff --git a/report/catalog-report-types.ts b/report/catalog-report-types.ts index 6f9efc623..5add89a71 100644 --- a/report/catalog-report-types.ts +++ b/report/catalog-report-types.ts @@ -1,5 +1,6 @@ import type { ConfigChange, ConfigAnalysis, ConfigRelationship, ConfigItem } from './config-types.ts'; import type { RBACResource } from './rbac-types.ts'; +import type { ScraperInfo } from './scraper-types.ts'; export interface CatalogReportSections { changes: boolean; @@ -53,6 +54,37 @@ export interface CatalogReportConfigGroup { accessLogs: CatalogReportAccessLog[]; } +export interface QueryLogEntry { + name: string; + args?: string; + count: number; + duration: number; + error?: string; + summary?: string; + pretty: string; +} + +export interface CatalogReportOptions { + title: string; + since: string; + sections: CatalogReportSections; + recursive: boolean; + groupBy: string; + changeArtifacts: boolean; + filters?: string[]; + thresholds?: { staleDays: number; reviewOverdueDays: number }; + categoryMappings?: Record; +} + +export interface CatalogReportAudit { + buildCommit: string; + buildVersion: string; + gitStatus?: string; + options: CatalogReportOptions; + scrapers: ScraperInfo[]; + queries: QueryLogEntry[]; +} + export interface CatalogReportData { title: string; generatedAt: string; @@ -61,6 +93,8 @@ export interface CatalogReportData { to?: string; recursive?: boolean; groupBy?: string; + categoryMappings?: Record; + thresholds?: { staleDays?: number; reviewOverdueDays?: number }; configItem: ConfigItem & { config?: string; name: string; @@ -88,6 +122,7 @@ export interface CatalogReportData { configGroups?: CatalogReportConfigGroup[]; relationshipTree?: CatalogReportTreeNode; entries?: CatalogReportEntry[]; + audit?: CatalogReportAudit; } export interface CatalogReportEntry { diff --git a/report/components/AuditPage.tsx b/report/components/AuditPage.tsx new file mode 100644 index 000000000..6825f86c1 --- /dev/null +++ b/report/components/AuditPage.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { Section, ListTable } from '@flanksource/facet'; +import type { CatalogReportAudit } from '../catalog-report-types.ts'; +import ScraperCard from './ScraperCard.tsx'; + +interface Props { + audit: CatalogReportAudit; +} + +function MetadataRow({ label, value }: { label: string; value?: string }) { + if (!value) return null; + return ( + + {label} + {value} + + ); +} + +function SectionBadge({ label, enabled }: { label: string; enabled: boolean }) { + return ( + + {label} + + ); +} + +export default function AuditPage({ audit }: Props) { + const opts = audit.options; + const sections = opts.sections; + + return ( + <> +
+ + + + + + + + + + {opts.thresholds && ( + <> + + + + )} + +
+ +
+ + + + + + +
+ + {(opts.filters || []).length > 0 && ( +
+
Filters
+ {opts.filters!.map((f, i) => ( +
{f}
+ ))} +
+ )} + + {opts.categoryMappings && Object.keys(opts.categoryMappings).length > 0 && ( +
+
Category Mappings
+ {Object.entries(opts.categoryMappings).map(([key, values]) => ( +
+ {key}: {values.join(', ')} +
+ ))} +
+ )} +
+ + {audit.gitStatus && ( +
+
+            {audit.gitStatus}
+          
+
+ )} + + {audit.queries.length > 0 && ( +
+ ({ + id: String(i), + subject: q.pretty, + count: String(q.count), + }))} + subject="subject" + keys={['count']} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt] font-mono" + /> +
+ )} + + {audit.scrapers.length > 0 && ( +
+
+ {audit.scrapers.map((s) => ( + + ))} +
+
+ )} + + ); +} diff --git a/report/components/BackupChanges.tsx b/report/components/BackupChanges.tsx index 909b5a4e1..593e4704a 100644 --- a/report/components/BackupChanges.tsx +++ b/report/components/BackupChanges.tsx @@ -60,6 +60,7 @@ export default function BackupChanges({ changes }: Props) { const completed = backupEvents.filter((change) => getBackupCalendarStatus(change) === 'success'); const failed = backupEvents.filter((change) => getBackupCalendarStatus(change) === 'failed'); const inProgress = backupEvents.filter((change) => getBackupCalendarStatus(change) === 'warning'); + const attentionEvents = [...failed, ...inProgress]; const latestSuccessful = completed.reduce((latest, change) => { if (!latest) return change; return new Date(change.date).getTime() > new Date(latest.date).getTime() ? change : latest; @@ -73,44 +74,56 @@ export default function BackupChanges({ changes }: Props) { return ( <> -
0 ? 'grid-cols-4' : 'grid-cols-3'} gap-[3mm] mb-[4mm]`}> - 0 ? 'Needs attention' : 'No failures'} - variant="summary" - size="sm" - color={failed.length > 0 ? 'red' : 'gray'} - valueClassName={COUNT_VALUE_CLASS} - /> - 0 ? 'orange' : 'gray'} - valueClassName={COUNT_VALUE_CLASS} - /> - - {restoreEvents.length > 0 && ( +
+
0 ? 'Needs attention' : 'No failures'} variant="summary" size="sm" - color="blue" + color={failed.length > 0 ? 'red' : 'gray'} + shrink valueClassName={COUNT_VALUE_CLASS} /> +
+
+ 0 ? 'orange' : 'gray'} + shrink + valueClassName={COUNT_VALUE_CLASS} + /> +
+
+ +
+ {restoreEvents.length > 0 && ( +
+ +
)}
@@ -120,11 +133,11 @@ export default function BackupChanges({ changes }: Props) {
)} - {attentionRows.length > 0 && ( + {attentionEvents.length > 0 && (

Exceptions & Running Jobs

({ + rows={attentionEvents.map((change) => ({ date: change.date, subject: change.description, subtitle: `Changed by ${getChangeActor(change)}`, @@ -176,6 +189,7 @@ export default function BackupChanges({ changes }: Props) {

Event Stream

({ + id: change.id, date: change.date, subject: change.description, subtitle: `Changed by ${getChangeActor(change)}`, @@ -189,6 +203,7 @@ export default function BackupChanges({ changes }: Props) { primaryTags={['type']} keys={['sourceLabel']} tagMapping={BACKUP_TAG_MAPPING} + groups={[{ by: 'date' }]} size="xs" density="compact" wrap diff --git a/report/components/BackupsSection.tsx b/report/components/BackupsSection.tsx index f7ea597e0..7e7860b74 100644 --- a/report/components/BackupsSection.tsx +++ b/report/components/BackupsSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, StatCard, CompactTable } from '@flanksource/facet'; +import { Section, StatCard, ListTable } from '@flanksource/facet'; import type { ApplicationBackup, ApplicationBackupRestore } from '../types.ts'; import { formatDateTime } from './utils.ts'; import BackupActivityCalendar from './BackupActivityCalendar.tsx'; @@ -10,6 +10,24 @@ interface Props { restores: ApplicationBackupRestore[]; } +const BACKUP_TAG_MAPPING = (key: string, value: unknown): string => { + if (key !== 'status') { + return ''; + } + + const normalized = String(value).toLowerCase(); + if (normalized.includes('fail')) { + return 'text-red-700 bg-red-50 border-red-200'; + } + if (normalized.includes('success')) { + return 'text-green-700 bg-green-50 border-green-200'; + } + if (normalized.includes('running') || normalized.includes('progress') || normalized.includes('started') || normalized.includes('queued')) { + return 'text-orange-700 bg-orange-50 border-orange-200'; + } + return 'text-gray-600 bg-gray-50 border-gray-200'; +}; + export default function BackupsSection({ backups, restores }: Props) { const successCount = backups.filter((b) => b.status === 'success').length; const failedCount = backups.filter((b) => b.status !== 'success').length; @@ -19,16 +37,7 @@ export default function BackupsSection({ backups, restores }: Props) { label: backup.size || undefined, })); - const failedRows = backups - .filter((b) => b.status !== 'success') - .map((b) => [b.database, formatDateTime(b.date), b.size, b.status]); - - const restoreRows = restores.map((r) => [ - r.database, - formatDateTime(r.date), - r.status, - formatDateTime(r.completedAt), - ]); + const failedRows = backups.filter((b) => b.status !== 'success'); return (
@@ -43,13 +52,51 @@ export default function BackupsSection({ backups, restores }: Props) { {failedRows.length > 0 && (

Failed Backups

- + ({ + subject: backup.database, + subtitle: backup.size || 'Size unavailable', + date: backup.date, + status: backup.status, + sourceLabel: `Source: ${backup.source || '-'}`, + }))} + subject="subject" + subtitle="subtitle" + date="date" + dateFormat="long" + primaryTags={['status']} + keys={['sourceLabel']} + tagMapping={BACKUP_TAG_MAPPING} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + />
)} - {restoreRows.length > 0 && ( + {restores.length > 0 && (
-

Restore History

- +

Restore Jobs

+ ({ + subject: restore.database, + subtitle: `Completed ${formatDateTime(restore.completedAt)}`, + date: restore.date, + status: restore.status, + sourceLabel: `Source: ${restore.source || '-'}`, + }))} + subject="subject" + subtitle="subtitle" + date="date" + dateFormat="long" + primaryTags={['status']} + keys={['sourceLabel']} + tagMapping={BACKUP_TAG_MAPPING} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + />
)}
diff --git a/report/components/ConfigChangesSection.tsx b/report/components/ConfigChangesSection.tsx index e2a8ae962..c360be5ee 100644 --- a/report/components/ConfigChangesSection.tsx +++ b/report/components/ConfigChangesSection.tsx @@ -43,7 +43,7 @@ function ChangeEntry({ change, dateFormat, hideConfigName }: { change: ConfigCha )} {change.summary ?? '-'} {sev !== 'info' && ( - + {sev} )} diff --git a/report/components/ConfigInsightsSection.tsx b/report/components/ConfigInsightsSection.tsx index ebe810546..753cb51d7 100644 --- a/report/components/ConfigInsightsSection.tsx +++ b/report/components/ConfigInsightsSection.tsx @@ -46,11 +46,11 @@ function InsightEntry({ analysis }: { analysis: ConfigAnalysis }) { {analysis.configName} )} {analysis.message || analysis.summary || '-'} - + {sev} {analysis.status && ( - + {analysis.status} )} diff --git a/report/components/ConfigTreeSection.tsx b/report/components/ConfigTreeSection.tsx index 6555c61d9..90855eb75 100644 --- a/report/components/ConfigTreeSection.tsx +++ b/report/components/ConfigTreeSection.tsx @@ -3,35 +3,32 @@ import { Section } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { CatalogReportTreeNode } from '../catalog-report-types.ts'; -const EDGE_STYLES: Record = { - parent: { border: 'border-l-blue-300' }, - child: { border: 'border-l-green-300' }, - related: { border: 'border-l-purple-300' }, - target: { border: 'border-l-blue-500' }, -}; - -function TreeNodeRow({ node, depth = 0 }: { node: CatalogReportTreeNode; depth?: number }) { - const style = EDGE_STYLES[node.edgeType || 'child'] || EDGE_STYLES.child; +function TreeNodeRow({ node, isRoot = false }: { node: CatalogReportTreeNode; isRoot?: boolean }) { const isTarget = node.edgeType === 'target'; const children = node.children || []; return ( -
-
- {node.type && } - +
+
+ {!isRoot && } + {node.type && } + {node.name} {node.type && ( - ({node.type}) + ({node.type}) )} {node.relation && ( - {node.relation} + {node.relation} )}
- {children.map((child, idx) => ( - - ))} + {children.length > 0 && ( +
+ {children.map((child, idx) => ( + + ))} +
+ )}
); } @@ -45,7 +42,7 @@ export default function ConfigTreeSection({ tree }: Props) { return (
- +
); } diff --git a/report/components/DeploymentChanges.tsx b/report/components/DeploymentChanges.tsx index a3b7d3637..180b8f881 100644 --- a/report/components/DeploymentChanges.tsx +++ b/report/components/DeploymentChanges.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { StatCard } from '@flanksource/facet'; +import { ListTable, StatCard } from '@flanksource/facet'; import type { ApplicationChange } from '../types.ts'; -import { formatEntryDate, getTimeBucket, type TimeBucketFormat } from './utils.ts'; import { classifyDeploymentChange, filterDeploymentChanges, @@ -12,36 +11,31 @@ interface Props { changes: ApplicationChange[]; } +const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; const CATEGORY_STYLES: Record<'scale' | 'policy' | 'spec', string> = { scale: 'bg-blue-50 text-blue-700 border-blue-200', policy: 'bg-orange-50 text-orange-700 border-orange-200', spec: 'bg-slate-50 text-slate-700 border-slate-200', }; - -interface BucketGroup { - key: string; - label: string; - dateFormat: TimeBucketFormat; - changes: ApplicationChange[]; -} - -function groupByTimeBucket(changes: ApplicationChange[]): BucketGroup[] { - const groups: BucketGroup[] = []; - const groupMap = new Map(); - - for (const change of changes) { - const bucket = getTimeBucket(change.date); - let group = groupMap.get(bucket.key); - if (!group) { - group = { key: bucket.key, label: bucket.label, dateFormat: bucket.dateFormat, changes: [] }; - groupMap.set(bucket.key, group); - groups.push(group); - } - group.changes.push(change); +const CATEGORY_LABELS: Record<'scale' | 'policy' | 'spec', string> = { + scale: 'Scale', + policy: 'Policy', + spec: 'Spec', +}; +const CATEGORY_TAG_MAPPING = (key: string, value: unknown): string => { + if (key !== 'category') { + return ''; } - return groups; -} + const normalized = String(value).toLowerCase(); + if (normalized === 'scale') { + return CATEGORY_STYLES.scale; + } + if (normalized === 'policy') { + return CATEGORY_STYLES.policy; + } + return CATEGORY_STYLES.spec; +}; export default function DeploymentChanges({ changes }: Props) { const relevant = filterDeploymentChanges(changes).sort((a, b) => ( @@ -58,43 +52,78 @@ export default function DeploymentChanges({ changes }: Props) { spec: relevant.filter((change) => classifyDeploymentChange(change) === 'spec').length, }; - const groups = groupByTimeBucket(relevant); - return ( <> -
- - - - -
- - {groups.map((group) => ( -
-
- {group.label} - ({group.changes.length}) -
-
- {group.changes.map((change) => { - const category = classifyDeploymentChange(change) ?? 'spec'; - return ( -
- - {formatEntryDate(change.date, group.dateFormat)} - - - {category} - - {change.changeType ?? '-'} - {getChangeActor(change)} - {change.description} -
- ); - })} -
+
+
+
- ))} +
+ +
+
+ +
+
+ +
+
+ { + const category = classifyDeploymentChange(change) ?? 'spec'; + return { + id: change.id, + date: change.date, + subject: change.description, + subtitle: change.changeType ?? '-', + category: CATEGORY_LABELS[category], + actor: getChangeActor(change), + }; + })} + subject="subject" + subtitle="subtitle" + date="date" + primaryTags={['category']} + keys={['actor']} + tagMapping={CATEGORY_TAG_MAPPING} + groups={[{ by: 'date' }]} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + /> ); } diff --git a/report/components/DynamicSection.tsx b/report/components/DynamicSection.tsx index 8ff685eb1..4b8931e87 100644 --- a/report/components/DynamicSection.tsx +++ b/report/components/DynamicSection.tsx @@ -92,7 +92,7 @@ function SeverityBadge({ severity }: { severity: string }) { const color = SEVERITY_COLORS[key] ?? '#6B7280'; const bg = SEVERITY_BG[key] ?? '#F3F4F6'; return ( - + {severity} ); diff --git a/report/components/GitRef.tsx b/report/components/GitRef.tsx new file mode 100644 index 000000000..c057c9dd1 --- /dev/null +++ b/report/components/GitRef.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Icon } from '@flanksource/icons/icon'; + +interface GitRefProps { + url?: string; + branch?: string; + file?: string; + dir?: string; + link?: string; + size?: 'xs' | 'sm'; +} + +const SIZE_CLASSES = { + xs: 'text-[5pt]', + sm: 'text-xs', +}; + +function Tag({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return ( + + {children} + + ); +} + +export default function GitRef({ url, branch, file, dir, link, size = 'xs' }: GitRefProps) { + if (!url && !file) return null; + const textClass = SIZE_CLASSES[size]; + + return ( + + + {url && {url}{branch ? ` @ ${branch}` : ''}} + {dir && {dir}/} + {file && {file}} + + ); +} + +export function GitRefFromSource({ gitops, size }: { gitops?: { git: { url: string; branch: string; file: string; dir: string; link: string } }; size?: 'xs' | 'sm' }) { + if (!gitops?.git?.url) return null; + return ; +} diff --git a/report/components/RBACChanges.tsx b/report/components/RBACChanges.tsx index a2c2da9e3..93605add7 100644 --- a/report/components/RBACChanges.tsx +++ b/report/components/RBACChanges.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { StatCard } from '@flanksource/facet'; +import { Badge, ListTable, StatCard } from '@flanksource/facet'; import type { ApplicationChange } from '../types.ts'; -import ConfigLink from './ConfigLink.tsx'; +import { ConfigTypeIcon } from './configTypeIcon.tsx'; import { IdentityIcon } from './rbac-visual.tsx'; -import { formatDateTime } from './utils.ts'; import { filterRBACChanges, groupRBACChanges, type RBACChangeRow } from './change-section-utils.ts'; interface Props { @@ -11,49 +10,19 @@ interface Props { } const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; -const ACTION_BADGE_CLASSES: Record<'Granted' | 'Revoked', string> = { - Granted: 'text-green-700 bg-green-50 border-green-200', - Revoked: 'text-red-700 bg-red-50 border-red-200', +const ACTION_BADGE_COLORS: Record<'Granted' | 'Revoked', { color: string; textColor: string; borderColor: string }> = { + Granted: { + color: 'bg-green-50', + textColor: 'text-green-700', + borderColor: 'border-green-200', + }, + Revoked: { + color: 'bg-red-50', + textColor: 'text-red-700', + borderColor: 'border-red-200', + }, }; -function PermissionRow({ row }: { row: RBACChangeRow }) { - const changedBy = row.changedBy !== '-' ? row.changedBy : row.source; - const roleLabel = row.role || 'Access'; - const identityRoleSource = row.subjectKind === 'group' ? `group:${row.subject}` : undefined; - - return ( -
-
-
- - {row.action} - - {roleLabel} - to - - - {row.subject} - - {row.viaGroup && ( - via {row.viaGroup} - )} - - {formatDateTime(row.date)} - -
-
- Changed by {changedBy} -
-
- {row.notes && ( -
- {row.notes} -
- )} -
- ); -} - export default function RBACChanges({ changes }: Props) { const relevant = filterRBACChanges(changes); if (!relevant.length) { @@ -61,6 +30,9 @@ export default function RBACChanges({ changes }: Props) { } const groups = groupRBACChanges(relevant); + const rows = groups + .flatMap((group) => group.rows.map((row) => ({ ...row, configName: group.configName, configType: group.configType }))) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); const grantedCount = groups.reduce((total, group) => total + group.rows.filter((row) => row.action === 'Granted').length, 0); const revokedCount = groups.reduce((total, group) => total + group.rows.filter((row) => row.action === 'Revoked').length, 0); const netCount = grantedCount - revokedCount; @@ -68,59 +40,98 @@ export default function RBACChanges({ changes }: Props) { return ( <> -
- 0 ? 'orange' : 'gray'} - valueClassName={COUNT_VALUE_CLASS} - /> - 0 ? 'green' : 'gray'} - valueClassName={COUNT_VALUE_CLASS} - /> - 0 ? `+${netCount}` : String(netCount)} - sublabel="Granted minus revoked" - variant="summary" - size="sm" - color={netColor} - valueClassName={COUNT_VALUE_CLASS} - /> +
+
+ 0 ? 'orange' : 'gray'} + shrink + valueClassName={COUNT_VALUE_CLASS} + /> +
+
+ 0 ? 'green' : 'gray'} + shrink + valueClassName={COUNT_VALUE_CLASS} + /> +
+
+ 0 ? `+${netCount}` : String(netCount)} + sublabel="Granted minus revoked" + variant="summary" + size="sm" + color={netColor} + shrink + valueClassName={COUNT_VALUE_CLASS} + /> +
-
- {groups.map((group) => ( -
-
-
- -
- {group.configType && ( - - {group.configType} + { + const identityRoleSource = row.subjectKind === 'group' ? `group:${row.subject}` : undefined; + return { + id: row.id, + date: row.date, + configName: row.configName, + configType: row.configType, + subject: ( + + + {row.role || 'Access'} + to + + + {row.subject} - )} - - {group.rows.length} change{group.rows.length === 1 ? '' : 's'} -
-
- {group.rows.map((row) => ( - - ))} -
-
- ))} -
+ ), + subtitle: row.viaGroup ? `via ${row.viaGroup}` : undefined, + changedByLabel: `Changed by ${row.changedBy !== '-' ? row.changedBy : row.source}`, + notes: row.notes, + }; + })} + subject="subject" + subtitle="subtitle" + body="notes" + date="date" + keys={['changedByLabel']} + groups={[{ by: 'date' }, { by: 'field', field: 'configName' }]} + iconRenderer={(_value, context) => { + if (context.kind !== 'group' || context.field !== 'configName') { + return null; + } + + const configType = context.group?.sampleRow?.configType; + return typeof configType === 'string' && configType + ? + : null; + }} + size="xs" + density="compact" + wrap + cellClassName="text-[8pt]" + /> ); } diff --git a/report/components/ScraperCard.tsx b/report/components/ScraperCard.tsx index 5d6a12170..f70fabaea 100644 --- a/report/components/ScraperCard.tsx +++ b/report/components/ScraperCard.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Icon } from '@flanksource/icons/icon'; import type { ScraperInfo } from '../scraper-types.ts'; import { formatDate } from './utils.ts'; +import { GitRefFromSource } from './GitRef.tsx'; const TYPE_ICONS: Record = { kubernetes: 'Kubernetes', @@ -18,72 +19,37 @@ const TYPE_ICONS: Record = { kubernetesFile: 'Kubernetes', }; +function Tag({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + interface Props { scraper: ScraperInfo; } export default function ScraperCard({ scraper }: Props) { - const gitops = scraper.gitops; - const hashShort = scraper.specHash ? scraper.specHash.slice(0, 12) : ''; - return ( -
- {/* Header: icons + name */} -
- {scraper.types.map((t) => ( - - ))} - {scraper.name} - {scraper.source && ( - - {scraper.source} - - )} -
- - {/* Metadata row */} -
- {hashShort && ( - - sha256:{hashShort} - - )} - {scraper.createdBy && ( - - {scraper.createdBy} - - )} - {scraper.createdAt && ( - - created {formatDate(scraper.createdAt)} - - )} - {scraper.updatedAt && ( - - updated {formatDate(scraper.updatedAt)} - - )} -
- - {/* GitOps provenance */} - {gitops && gitops.git.url && ( -
- - - {gitops.git.url} - - {gitops.git.branch && ( - - {gitops.git.branch} - - )} - {gitops.git.file && ( - - {gitops.git.file} - - )} -
+
+ {(scraper.types || []).map((t) => ( + + ))} + {scraper.name} + {scraper.source && ( + + {scraper.source} + )} + {scraper.id.slice(0, 8)} + {scraper.createdBy && {scraper.createdBy}} + {scraper.updatedAt + ? modified {formatDate(scraper.updatedAt)} + : scraper.createdAt && created {formatDate(scraper.createdAt)} + } +
); } diff --git a/report/components/change-section-utils.ts b/report/components/change-section-utils.ts index 42ed6c0ac..660f8df66 100644 --- a/report/components/change-section-utils.ts +++ b/report/components/change-section-utils.ts @@ -1,4 +1,5 @@ import type { ApplicationChange, ApplicationPermissionChange } from '../types.ts'; +import type { ConfigChange } from '../config-types.ts'; export type ChangeSectionVariant = 'generic' | 'rbac' | 'backup' | 'deployment'; export type BackupCalendarStatus = 'success' | 'failed' | 'warning'; @@ -13,6 +14,9 @@ export interface BackupCalendarEntry { export interface RBACChangeRow { id: string; date: string; + configId?: string; + configName: string; + configType?: string; action: 'Granted' | 'Revoked'; subject: string; subjectKind: 'user' | 'group'; @@ -38,7 +42,7 @@ const BACKUP_SUCCESS_TYPES = new Set(['BackupCompleted', 'BackupSuccessful']); const BACKUP_FAILED_TYPES = new Set(['BackupFailed']); const BACKUP_PROGRESS_TYPES = new Set(['BackupStarted', 'BackupRunning', 'BackupEnqueued']); const RESTORE_CHANGE_TYPES = new Set(['BackupRestored', 'RestoreCompleted']); -const DEPLOYMENT_CHANGE_TYPES = new Set(['ScalingReplicaSet', 'PolicyUpdate']); +const DEPLOYMENT_CHANGE_TYPES = new Set(['ScalingReplicaSet', 'PolicyUpdate', 'CodeDeployment', 'diff']); function normalizedType(change: ApplicationChange): string { return change.changeType ?? ''; @@ -48,7 +52,54 @@ export function getChangeActor(change: ApplicationChange): string { return change.createdBy || change.source || '-'; } +function getCategoryKey(change: ApplicationChange): string { + return change.category ?? ''; +} + +type ChangeMappingInput = { + changeType?: string; + severity?: string; + status?: string; +}; + +function getRuleSeverity(rule: string): string | undefined { + const parts = rule.split('@'); + if (parts.length !== 2) { + return undefined; + } + return cleanField(parts[1])?.toLowerCase(); +} + +function getRuleType(rule: string): string { + return rule.split('@', 2)[0]?.trim() ?? ''; +} + +function getChangeSeverity(change: ChangeMappingInput): string { + return (change.status ?? change.severity ?? '').trim().toLowerCase(); +} + +function matchesCategoryRule(change: ChangeMappingInput, rule: string): boolean { + if ((change.changeType ?? '') !== getRuleType(rule)) { + return false; + } + + const ruleSeverity = getRuleSeverity(rule); + if (!ruleSeverity) { + return true; + } + + return getChangeSeverity(change) === ruleSeverity; +} + function normalizeRBACAction(change: ApplicationChange): RBACChangeAction | null { + const category = getCategoryKey(change); + if (category === 'rbac.granted') { + return 'added'; + } + if (category === 'rbac.revoked') { + return 'removed'; + } + const type = normalizedType(change); if (RBAC_ADDED_TYPES.has(type)) { return 'added'; @@ -64,38 +115,50 @@ export function isRBACChange(change: ApplicationChange): boolean { } export function isBackupChange(change: ApplicationChange): boolean { + if (getCategoryKey(change).startsWith('backup.')) { + return true; + } + const type = normalizedType(change); return BACKUP_SUCCESS_TYPES.has(type) || BACKUP_FAILED_TYPES.has(type) || BACKUP_PROGRESS_TYPES.has(type) || RESTORE_CHANGE_TYPES.has(type); } export function isRestoreChange(change: ApplicationChange): boolean { + if (getCategoryKey(change) === 'backup.restore') { + return true; + } + return RESTORE_CHANGE_TYPES.has(normalizedType(change)); } export function classifyDeploymentChange(change: ApplicationChange): 'scale' | 'policy' | 'spec' | null { + const category = getCategoryKey(change); + if (category.startsWith('deployment.')) { + const suffix = category.slice('deployment.'.length); + if (suffix === 'scale' || suffix === 'scaling') { + return 'scale'; + } + if (suffix === 'policy') { + return 'policy'; + } + return 'spec'; + } + if (category === 'deployment') { + return 'spec'; + } + const type = normalizedType(change); - const description = change.description.toLowerCase(); const lowerType = type.toLowerCase(); - if (type === 'ScalingReplicaSet' || lowerType.includes('replicaset') || /scaled|scaling|replica/.test(description)) { + if (type === 'ScalingReplicaSet' || lowerType.includes('replicaset')) { return 'scale'; } - if (type === 'PolicyUpdate' || description.includes('policy')) { + if (type === 'PolicyUpdate') { return 'policy'; } - if ( - DEPLOYMENT_CHANGE_TYPES.has(type) || - (type === 'diff' && ( - description.includes('deployment') || - description.includes('rollout') || - description.includes('image updated') - )) || - description.includes('deployment') || - description.includes('rollout') || - description.includes('image updated') - ) { + if (DEPLOYMENT_CHANGE_TYPES.has(type)) { return 'spec'; } @@ -118,41 +181,98 @@ export function filterDeploymentChanges(changes: ApplicationChange[]): Applicati return changes.filter(isDeploymentChange); } -export function inferChangeSectionVariant(title: string, changes: ApplicationChange[]): ChangeSectionVariant { - const lowerTitle = title.toLowerCase(); +function resolveCategoryMappings(categoryMappings?: Record): Record | undefined { + if (!categoryMappings || Object.keys(categoryMappings).length === 0) { + return undefined; + } + return categoryMappings; +} - if (/\brbac\b|\bpermission/.test(lowerTitle)) { - return 'rbac'; +function findCategoryForChange(change: ChangeMappingInput, categoryMappings?: Record): string | undefined { + const mappings = resolveCategoryMappings(categoryMappings); + if (!mappings) { + return undefined; } - if (/\bbackup\b|\brestore\b/.test(lowerTitle)) { - return 'backup'; + for (const [category, rules] of Object.entries(mappings)) { + if (rules.some((rule) => getRuleSeverity(rule) && matchesCategoryRule(change, rule))) { + return category; + } } - if (/\bdeployment\b|\brollout\b/.test(lowerTitle)) { - return 'deployment'; + for (const [category, types] of Object.entries(mappings)) { + if (types.some((rule) => !getRuleSeverity(rule) && matchesCategoryRule(change, rule))) { + return category; + } } - const rbacCount = filterRBACChanges(changes).length; - const backupCount = filterBackupChanges(changes).length; - const deploymentCount = filterDeploymentChanges(changes).length; + return undefined; +} + +export function inferChangeSectionVariant( + title: string, + changes: ApplicationChange[], + categoryMappings?: Record, +): ChangeSectionVariant { + const lowerTitle = title.toLowerCase(); - if (rbacCount > 0 && rbacCount === changes.length) { + if (/\brbac\b|\bpermission/.test(lowerTitle)) { return 'rbac'; } - if (backupCount > 0 && backupCount === changes.length) { + if (/\bbackup\b|\brestore\b/.test(lowerTitle)) { return 'backup'; } - if (deploymentCount > 0 && deploymentCount >= Math.ceil(changes.length / 2)) { + if (/\bdeployment\b|\brollout\b/.test(lowerTitle)) { return 'deployment'; } + const mappings = resolveCategoryMappings(categoryMappings); + if (mappings) { + let rbacCount = 0; + let backupCount = 0; + let deploymentCount = 0; + for (const change of changes) { + const category = findCategoryForChange(change, mappings); + if (!category) { + continue; + } + if (category === 'rbac' || category.startsWith('rbac.')) { + rbacCount += 1; + } else if (category === 'backup' || category.startsWith('backup.')) { + backupCount += 1; + } else if (category === 'deployment' || category.startsWith('deployment.')) { + deploymentCount += 1; + } + } + + if (rbacCount > 0 && rbacCount === changes.length) { + return 'rbac'; + } + if (backupCount > 0 && backupCount === changes.length) { + return 'backup'; + } + if (deploymentCount > 0 && deploymentCount >= Math.ceil(changes.length / 2)) { + return 'deployment'; + } + } + return 'generic'; } export function getBackupCalendarStatus(change: ApplicationChange): BackupCalendarStatus | null { + const category = getCategoryKey(change); + if (category === 'backup.success') { + return 'success'; + } + if (category === 'backup.failed') { + return 'failed'; + } + if (category === 'backup.progress') { + return 'warning'; + } + const type = normalizedType(change); if (BACKUP_SUCCESS_TYPES.has(type)) { return 'success'; @@ -346,6 +466,9 @@ export function groupRBACChanges(changes: ApplicationChange[]): RBACChangeGroup[ group.rows.push({ id: change.id, date: change.date, + configId, + configName, + configType: cleanField(change.configType), action: action === 'added' ? 'Granted' : 'Revoked', subject, subjectKind, @@ -364,3 +487,55 @@ export function groupRBACChanges(changes: ApplicationChange[]): RBACChangeGroup[ })) .sort((a, b) => new Date(b.latestDate).getTime() - new Date(a.latestDate).getTime()); } + +export interface CategorizedChanges { + rbac: Array<{ change: ConfigChange; category: string }>; + backup: Array<{ change: ConfigChange; category: string }>; + deployment: Array<{ change: ConfigChange; category: string }>; + uncategorized: ConfigChange[]; +} + +export function categorizeChanges( + changes: ConfigChange[], + categoryMappings?: Record, +): CategorizedChanges { + const result: CategorizedChanges = { rbac: [], backup: [], deployment: [], uncategorized: [] }; + const mappings = resolveCategoryMappings(categoryMappings); + if (!mappings) { + result.uncategorized = changes; + return result; + } + + for (const change of changes) { + const category = findCategoryForChange(change, mappings); + if (!category) { + result.uncategorized.push(change); + continue; + } + + if (category === 'rbac' || category.startsWith('rbac.')) result.rbac.push({ change, category }); + else if (category === 'backup' || category.startsWith('backup.')) result.backup.push({ change, category }); + else if (category === 'deployment' || category.startsWith('deployment.')) result.deployment.push({ change, category }); + else result.uncategorized.push(change); + } + return result; +} + +export function configChangeToApplicationChange(c: ConfigChange, category?: string): ApplicationChange { + const permission = c.details?.permission as ApplicationPermissionChange | undefined; + return { + id: c.id ?? '', + date: c.createdAt ?? '', + changeType: c.changeType, + category, + source: c.source, + createdBy: c.createdBy ?? c.externalCreatedBy, + configId: c.configID, + configName: c.configName, + configType: c.configType, + permission, + description: c.summary ?? '', + status: c.severity ?? 'info', + createdAt: c.createdAt ?? '', + }; +} diff --git a/report/config-types.ts b/report/config-types.ts index 80619723f..33343b4d8 100644 --- a/report/config-types.ts +++ b/report/config-types.ts @@ -38,6 +38,7 @@ export interface ConfigChange { severity?: ConfigSeverity; source?: string; summary?: string; + details?: Record; createdBy?: string; externalCreatedBy?: string; createdAt?: string; diff --git a/report/facet.go b/report/facet.go index c67636be7..51ba871e6 100644 --- a/report/facet.go +++ b/report/facet.go @@ -12,14 +12,29 @@ import ( "os" "os/exec" "path/filepath" + "strings" "sync" commonshttp "github.com/flanksource/commons/http" + "github.com/flanksource/commons/logger" "github.com/flanksource/duty/context" ) +// RenderResult contains the rendered output and metadata about the render. +type RenderResult struct { + Data []byte + SrcDir string + Entry string + DataFile string +} + // RenderCLI renders data to the given format using the local facet CLI binary. -func RenderCLI(data any, format, entryFile string) ([]byte, error) { +// With -v (log level 1): prints the facet command and tees stdout/stderr. +// With -vv (log level 2): also keeps the data file and report dir for re-rendering. +func RenderCLI(data any, format, entryFile string) (*RenderResult, error) { + verbose := logger.IsLevelEnabled(1) + keepFiles := logger.IsLevelEnabled(2) + facetBin, err := exec.LookPath("facet") if err != nil { return nil, fmt.Errorf("facet not found on PATH: install with 'npm install -g @flanksource/facet'") @@ -42,7 +57,9 @@ func RenderCLI(data any, format, entryFile string) ([]byte, error) { if err != nil { return nil, fmt.Errorf("create data temp file: %w", err) } - defer os.Remove(dataFile.Name()) + if !keepFiles { + defer os.Remove(dataFile.Name()) + } if _, err := dataFile.Write(dataJSON); err != nil { return nil, fmt.Errorf("write data file: %w", err) @@ -59,13 +76,34 @@ func RenderCLI(data any, format, entryFile string) ([]byte, error) { var stderr bytes.Buffer cmd := exec.Command(facetBin, format, entryFile, "-d", dataFile.Name(), "-o", outFile.Name()) cmd.Dir = srcDir - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("facet %s failed: %w\n%s", format, err, stderr.String()) + if verbose { + fmt.Fprintf(os.Stderr, "$ cd %s\n", srcDir) + fmt.Fprintf(os.Stderr, "$ %s\n", strings.Join(cmd.Args, " ")) + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + } else { + cmd.Stderr = &stderr + } + + if err = cmd.Run(); err != nil { + if stderr.Len() > 0 { + return nil, fmt.Errorf("facet %s failed: %w\n%s", format, err, stderr.String()) + } + return nil, fmt.Errorf("facet %s failed: %w", format, err) + } + + result, err := os.ReadFile(outFile.Name()) + if err != nil { + return nil, err } - return os.ReadFile(outFile.Name()) + return &RenderResult{ + Data: result, + SrcDir: srcDir, + Entry: entryFile, + DataFile: dataFile.Name(), + }, nil } // RenderHTTP renders data via a remote facet rendering service. diff --git a/report/kitchen-sink/ChangesPage.tsx b/report/kitchen-sink/ChangesPage.tsx index c9c27c6e7..2a6c9fa9d 100644 --- a/report/kitchen-sink/ChangesPage.tsx +++ b/report/kitchen-sink/ChangesPage.tsx @@ -5,6 +5,7 @@ import ConfigChangesSection from '../components/ConfigChangesSection.tsx'; import RBACChanges from '../components/RBACChanges.tsx'; import BackupChanges from '../components/BackupChanges.tsx'; import DeploymentChanges from '../components/DeploymentChanges.tsx'; +import { categorizeChanges, configChangeToApplicationChange } from '../components/change-section-utils.ts'; interface Props { data: KitchenSinkData; @@ -15,14 +16,40 @@ export default function ChangesPage({ data, pageProps }: Props) { const rbacChanges = data.rbacChanges ?? []; const backupChanges = data.backupChanges ?? []; const deploymentChanges = data.deploymentChanges ?? []; + const categoryMappings = (data as any).categoryMappings as Record | undefined; + const categorized = categorizeChanges(data.changes ?? [], categoryMappings); return ( +
+
+ A single changes array auto-split into specialized sections using categoryMappings. RBAC, backup, and deployment changes get their own renderers; the rest falls through to ConfigChangesSection. +
+ {categorized.rbac.length > 0 && ( +
+ configChangeToApplicationChange(change, category))} /> +
+ )} + {categorized.backup.length > 0 && ( +
+ configChangeToApplicationChange(change, category))} /> +
+ )} + {categorized.deployment.length > 0 && ( +
+ configChangeToApplicationChange(change, category))} /> +
+ )} + {categorized.uncategorized.length > 0 && ( + + )} +
+
- Groups permission changes by config and renders granted/revoked audit rows with role, principal, timestamp, and changed-by attribution. + Groups permission changes by date and resource, shows config type icons in resource headers, and renders compact granted/revoked audit rows with role, principal, timestamp, and changed-by attribution.
diff --git a/report/kitchen-sink/DynamicSectionsPage.tsx b/report/kitchen-sink/DynamicSectionsPage.tsx index 9e58c447a..9b0694c54 100644 --- a/report/kitchen-sink/DynamicSectionsPage.tsx +++ b/report/kitchen-sink/DynamicSectionsPage.tsx @@ -15,7 +15,7 @@ export default function DynamicSectionsPage({ data, pageProps }: Props) {
- DynamicSection chooses the specialized renderer from the section title and change type mix. + DynamicSection chooses the specialized renderer from the section title and change type mix, including grouped RBAC sections with date buckets and resource icons.
diff --git a/report/testdata/kitchen-sink.yaml b/report/testdata/kitchen-sink.yaml index 16f86037a..7f356d62d 100644 --- a/report/testdata/kitchen-sink.yaml +++ b/report/testdata/kitchen-sink.yaml @@ -14,6 +14,34 @@ configItem: createdAt: "2025-03-15T09:00:00Z" updatedAt: "2026-03-28T12:00:00Z" +categoryMappings: + rbac.granted: + - PermissionGranted + - PermissionAdded + - IAMRoleAdded + rbac.revoked: + - PermissionRevoked + - PermissionRemoved + - IAMRoleRemoved + backup.success: + - BackupCompleted + - BackupSuccessful + backup.failed: + - BackupFailed + backup.progress: + - BackupStarted + - BackupRunning + - BackupEnqueued + backup.restore: + - BackupRestored + - RestoreCompleted + deployment.spec: + - diff + deployment.scale: + - ScalingReplicaSet + deployment.policy: + - PolicyUpdate + changes: - id: "chg-001" configID: "cfg-eks-001" @@ -133,6 +161,56 @@ changes: createdBy: "bob@flanksource.com" createdAt: "2026-03-24T14:00:00Z" + - id: "chg-014" + configID: "cfg-eks-001" + changeType: "PermissionGranted" + severity: "info" + source: "okta" + summary: "Granted db_owner to alice@flanksource.com on prod-rds-01" + createdBy: "admin@flanksource.com" + createdAt: "2026-03-28T09:00:00Z" + details: + permission: + user: "alice@flanksource.com" + role: "db_owner" + + - id: "chg-015" + configID: "cfg-eks-001" + changeType: "PermissionRevoked" + severity: "info" + source: "okta" + summary: "Revoked Secrets Reader access for bob@flanksource.com on prod-eks-cluster" + createdBy: "admin@flanksource.com" + createdAt: "2026-03-27T15:00:00Z" + details: + permission: + user: "bob@flanksource.com" + role: "Secrets Reader" + + - id: "chg-016" + configID: "cfg-eks-001" + changeType: "BackupCompleted" + severity: "info" + source: "velero" + summary: "Full cluster backup completed successfully (2.4 GiB)" + createdAt: "2026-03-29T03:00:00Z" + + - id: "chg-017" + configID: "cfg-eks-001" + changeType: "BackupFailed" + severity: "high" + source: "velero" + summary: "Incremental backup failed: PVC snapshot timeout after 300s" + createdAt: "2026-03-28T03:00:00Z" + + - id: "chg-018" + configID: "cfg-eks-001" + changeType: "BackupStarted" + severity: "info" + source: "velero" + summary: "Scheduled backup initiated for prod-eks-cluster" + createdAt: "2026-03-30T03:00:00Z" + analyses: - id: "ana-001" configID: "cfg-eks-001" @@ -424,6 +502,21 @@ rbacChanges: status: "info" createdAt: "2026-03-29T18:40:00Z" + - id: "rbac-006" + date: "2026-03-29T11:15:00Z" + changeType: "PermissionAdded" + source: "okta" + createdBy: "governance-bot" + configId: "cfg-keyvault-001" + configName: "prod-keyvault" + configType: "Azure::KeyVault" + permission: + user: "ops-auditor" + role: "Secrets Reader" + description: "PermissionAdded: user ops-auditor, role Secrets Reader" + status: "info" + createdAt: "2026-03-29T11:15:00Z" + - id: "rbac-003" date: "2026-03-29T16:00:00Z" changeType: "AccessReviewed" @@ -1538,6 +1631,20 @@ dynamicSections: description: "PermissionRemoved: user contractor-temp, role db_datareader" status: "info" createdAt: "2026-03-29T18:40:00Z" + - id: "rbac-006" + date: "2026-03-29T11:15:00Z" + changeType: "PermissionAdded" + source: "okta" + createdBy: "governance-bot" + configId: "cfg-keyvault-001" + configName: "prod-keyvault" + configType: "Azure::KeyVault" + permission: + user: "ops-auditor" + role: "Secrets Reader" + description: "PermissionAdded: user ops-auditor, role Secrets Reader" + status: "info" + createdAt: "2026-03-29T11:15:00Z" - id: "rbac-004" date: "2026-03-28T13:05:00Z" changeType: "PermissionGranted" diff --git a/report/types.ts b/report/types.ts index 03e874b10..423e659c7 100644 --- a/report/types.ts +++ b/report/types.ts @@ -137,6 +137,7 @@ export interface ApplicationChange { id: string; date: string; changeType?: string; + category?: string; source?: string; createdBy?: string; configId?: string; From d1cede8c52f4475a47ba64e2111e685264e4cdac Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 10 Apr 2026 08:34:36 +0300 Subject: [PATCH 33/48] refactor(ui): replace category mapping record type with structured interface Changed categoryMappings from Record to CatalogReportCategoryMapping[] array. This provides a more explicit schema with category, filter, and transform fields instead of implicit type-based routing rules. Updated categorizeChanges to use change.category field directly. --- report/CatalogReport.tsx | 10 +- report/catalog-report-types.ts | 10 +- report/components/ApplicationDetails.tsx | 19 +- report/components/AuditPage.tsx | 11 +- report/components/BackupActivityCalendar.tsx | 2 +- report/components/BackupChanges.tsx | 19 +- report/components/BackupsSection.tsx | 15 +- report/components/ConfigChangesExamples.tsx | 71 +++ report/components/ConfigChangesSection.tsx | 225 ++++++++-- report/components/ConfigInsightsSection.tsx | 16 +- report/components/DeploymentChanges.tsx | 11 +- report/components/FindingsSection.tsx | 16 +- report/components/IncidentsSection.tsx | 16 +- report/components/RBACChanges.tsx | 9 +- report/components/RBACSummarySection.tsx | 44 +- report/components/change-section-utils.ts | 432 ++++++++++++++----- report/config-types.ts | 7 + report/kitchen-sink-data.ts | 6 +- report/kitchen-sink/ChangesPage.tsx | 12 +- report/mission-control.ts | 6 +- report/testdata/kitchen-sink.yaml | 234 ++++++++-- 21 files changed, 941 insertions(+), 250 deletions(-) create mode 100644 report/components/ConfigChangesExamples.tsx diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index 093c2096f..eeaf0cba9 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Document, Page, Header, Footer, Section } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; -import type { CatalogReportData, CatalogReportConfigGroup } from './catalog-report-types.ts'; +import type { CatalogReportData, CatalogReportConfigGroup, CatalogReportCategoryMapping } from './catalog-report-types.ts'; import type { ConfigChange } from './config-types.ts'; import ConfigChangesSection from './components/ConfigChangesSection.tsx'; import ConfigInsightsSection from './components/ConfigInsightsSection.tsx'; @@ -73,7 +73,7 @@ function ConfigJSONSection({ json }: { json: string }) { function CategorizedChangesSection({ changes, categoryMappings, hideConfigName }: { changes?: ConfigChange[]; - categoryMappings?: Record; + categoryMappings?: CatalogReportCategoryMapping[]; hideConfigName?: boolean; }) { if (!changes?.length) return null; @@ -82,17 +82,17 @@ function CategorizedChangesSection({ changes, categoryMappings, hideConfigName } <> {rbac.length > 0 && (
- configChangeToApplicationChange(change, category))} /> + configChangeToApplicationChange(change))} />
)} {backup.length > 0 && (
- configChangeToApplicationChange(change, category))} /> + configChangeToApplicationChange(change))} />
)} {deployment.length > 0 && (
- configChangeToApplicationChange(change, category))} /> + configChangeToApplicationChange(change))} />
)} {uncategorized.length > 0 && ( diff --git a/report/catalog-report-types.ts b/report/catalog-report-types.ts index 5add89a71..fc46af3f2 100644 --- a/report/catalog-report-types.ts +++ b/report/catalog-report-types.ts @@ -2,6 +2,12 @@ import type { ConfigChange, ConfigAnalysis, ConfigRelationship, ConfigItem } fro import type { RBACResource } from './rbac-types.ts'; import type { ScraperInfo } from './scraper-types.ts'; +export interface CatalogReportCategoryMapping { + category?: string; + filter: string; + transform?: string; +} + export interface CatalogReportSections { changes: boolean; insights: boolean; @@ -73,7 +79,7 @@ export interface CatalogReportOptions { changeArtifacts: boolean; filters?: string[]; thresholds?: { staleDays: number; reviewOverdueDays: number }; - categoryMappings?: Record; + categoryMappings?: CatalogReportCategoryMapping[]; } export interface CatalogReportAudit { @@ -93,7 +99,7 @@ export interface CatalogReportData { to?: string; recursive?: boolean; groupBy?: string; - categoryMappings?: Record; + categoryMappings?: CatalogReportCategoryMapping[]; thresholds?: { staleDays?: number; reviewOverdueDays?: number }; configItem: ConfigItem & { config?: string; diff --git a/report/components/ApplicationDetails.tsx b/report/components/ApplicationDetails.tsx index d707ce8ea..289022d6f 100644 --- a/report/components/ApplicationDetails.tsx +++ b/report/components/ApplicationDetails.tsx @@ -7,6 +7,8 @@ interface Props { app: Application; } +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; + export default function ApplicationDetails({ app }: Props) { const sortedProps = [...(app.properties ?? [])].sort( (a, b) => (a.order ?? 0) - (b.order ?? 0) @@ -18,15 +20,16 @@ export default function ApplicationDetails({ app }: Props) {

{app.description}

)} {sortedProps.length > 0 && ( -
+
{sortedProps.map((prop) => ( - +
+ +
))}
)} diff --git a/report/components/AuditPage.tsx b/report/components/AuditPage.tsx index 6825f86c1..287d853ec 100644 --- a/report/components/AuditPage.tsx +++ b/report/components/AuditPage.tsx @@ -68,12 +68,15 @@ export default function AuditPage({ audit }: Props) {
)} - {opts.categoryMappings && Object.keys(opts.categoryMappings).length > 0 && ( + {opts.categoryMappings && opts.categoryMappings.length > 0 && (
Category Mappings
- {Object.entries(opts.categoryMappings).map(([key, values]) => ( -
- {key}: {values.join(', ')} + {opts.categoryMappings.map((mapping, index) => ( +
+ {mapping.category && {mapping.category}} + {mapping.category && : } + {mapping.filter} + {mapping.transform && => {mapping.transform}}
))}
diff --git a/report/components/BackupActivityCalendar.tsx b/report/components/BackupActivityCalendar.tsx index 4df4dd2d6..2a512301e 100644 --- a/report/components/BackupActivityCalendar.tsx +++ b/report/components/BackupActivityCalendar.tsx @@ -90,7 +90,7 @@ export default function BackupActivityCalendar({ entries }: Props) { ]; return ( -
+

{monthLabel}

{DAY_HEADERS.map((day) => ( diff --git a/report/components/BackupChanges.tsx b/report/components/BackupChanges.tsx index 593e4704a..a39bfc9c7 100644 --- a/report/components/BackupChanges.tsx +++ b/report/components/BackupChanges.tsx @@ -17,6 +17,7 @@ interface Props { const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; const TIMESTAMP_VALUE_CLASS = 'text-[8pt] leading-[10pt]'; +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; const BACKUP_TAG_MAPPING = (key: string, value: unknown): string => { if (key !== 'state' && key !== 'type') { return ''; @@ -74,8 +75,8 @@ export default function BackupChanges({ changes }: Props) { return ( <> -
-
+
+
-
+
-
+
{restoreEvents.length > 0 && ( -
+
{backupEvents.length > 0 && ( -
+
)} {attentionEvents.length > 0 && (
-

Exceptions & Running Jobs

+

Exceptions & Running Jobs

({ date: change.date, @@ -161,7 +162,7 @@ export default function BackupChanges({ changes }: Props) { {restoreEvents.length > 0 && (
-

Restore Jobs

+

Restore Jobs

({ date: change.date, @@ -186,7 +187,7 @@ export default function BackupChanges({ changes }: Props) { )}
-

Event Stream

+

Event Stream

({ id: change.id, diff --git a/report/components/BackupsSection.tsx b/report/components/BackupsSection.tsx index 7e7860b74..d18baeb3f 100644 --- a/report/components/BackupsSection.tsx +++ b/report/components/BackupsSection.tsx @@ -10,6 +10,7 @@ interface Props { restores: ApplicationBackupRestore[]; } +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; const BACKUP_TAG_MAPPING = (key: string, value: unknown): string => { if (key !== 'status') { return ''; @@ -41,10 +42,16 @@ export default function BackupsSection({ backups, restores }: Props) { return (
-
- - - +
+
+ +
+
+ +
+
+ +
diff --git a/report/components/ConfigChangesExamples.tsx b/report/components/ConfigChangesExamples.tsx new file mode 100644 index 000000000..95158d0dd --- /dev/null +++ b/report/components/ConfigChangesExamples.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Section } from '@flanksource/facet'; +import type { ConfigChange } from '../config-types.ts'; +import ConfigChangesSection from './ConfigChangesSection.tsx'; + +interface Props { + changes?: ConfigChange[]; +} + +function pickMatching(changes: ConfigChange[], predicate: (change: ConfigChange) => boolean, limit: number): ConfigChange[] { + return changes.filter(predicate).slice(0, limit); +} + +export default function ConfigChangesExamples({ changes }: Props) { + if (!changes?.length) { + return null; + } + + const singleLine = pickMatching( + changes, + (change) => !change.summary || change.summary.length <= 72 || Boolean(change.typedChange?.kind), + 6, + ); + const typedDiffs = pickMatching( + changes, + (change) => ['Deployment/v1', 'Promotion/v1', 'Rollback/v1', 'Scaling/v1', 'CostChange/v1'].includes(change.typedChange?.kind ?? ''), + 5, + ); + const visualStates = pickMatching( + changes, + (change) => ( + (change.severity && change.severity !== 'info') + || Boolean(change.artifacts?.length) + || (change.changeType || '').toLowerCase().includes('backup') + || (change.changeType || '').toLowerCase().includes('permission') + || change.typedChange?.kind === 'Screenshot/v1' + ), + 6, + ); + + return ( + <> + {singleLine.length > 0 && ( +
+
+ Compact rows optimized for one-line scanning. Change type, diff chips, config, actor, counters, and severity stay inline whenever the summary is short enough. +
+ +
+ )} + + {typedDiffs.length > 0 && ( +
+
+ Typed changes show richer before/after chips for images, environments, versions, replicas, and costs instead of a generic diff label. +
+ +
+ )} + + {visualStates.length > 0 && ( +
+
+ Permission, backup, artifact, release, and higher-severity changes now use distinct badge accents to separate activity types at a glance. +
+ +
+ )} + + ); +} diff --git a/report/components/ConfigChangesSection.tsx b/report/components/ConfigChangesSection.tsx index c360be5ee..7739b7004 100644 --- a/report/components/ConfigChangesSection.tsx +++ b/report/components/ConfigChangesSection.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { Section, SeverityStatCard } from '@flanksource/facet'; +import { Badge, Section, SeverityStatCard } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ConfigChange, ConfigSeverity } from '../config-types.ts'; +import { getChangeTypeLabel, getTypedChangeDisplay } from './change-section-utils.ts'; import { getTimeBucket, formatEntryDate, type TimeBucketFormat } from './utils.ts'; interface Props { @@ -24,38 +25,199 @@ const SEVERITY_TEXT: Record = { low: 'text-blue-700 bg-blue-50 border-blue-200', info: 'text-gray-600 bg-gray-50 border-gray-200', }; +const SEVERITY_ACCENT_TEXT: Record = { + critical: 'text-red-600', + high: 'text-orange-600', + medium: 'text-yellow-700', + low: 'text-blue-600', + info: 'text-gray-500', +}; +type ChangeBadgeStyle = { color: string; textColor: string; borderColor: string }; +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; + +const CHANGE_BADGE_STYLES: Record = { + default: { color: 'bg-slate-100', textColor: 'text-slate-700', borderColor: 'border-slate-200' }, + diff: { color: 'bg-indigo-50', textColor: 'text-indigo-700', borderColor: 'border-indigo-200' }, + policy: { color: 'bg-orange-50', textColor: 'text-orange-700', borderColor: 'border-orange-200' }, + scale: { color: 'bg-sky-50', textColor: 'text-sky-700', borderColor: 'border-sky-200' }, + backup: { color: 'bg-emerald-50', textColor: 'text-emerald-700', borderColor: 'border-emerald-200' }, + permission: { color: 'bg-violet-50', textColor: 'text-violet-700', borderColor: 'border-violet-200' }, + release: { color: 'bg-fuchsia-50', textColor: 'text-fuchsia-700', borderColor: 'border-fuchsia-200' }, + artifact: { color: 'bg-cyan-50', textColor: 'text-cyan-700', borderColor: 'border-cyan-200' }, + cost: { color: 'bg-amber-50', textColor: 'text-amber-700', borderColor: 'border-amber-200' }, +}; + +function getChangeAccent(change: ConfigChange, label: string): ChangeBadgeStyle { + const kind = change.typedChange?.kind ?? ''; + const type = (change.changeType || '').toLowerCase(); + const category = (change.category || '').toLowerCase(); + const normalizedLabel = label.toLowerCase(); + + if (kind === 'Screenshot/v1' || type.includes('screenshot')) return CHANGE_BADGE_STYLES.artifact; + if (kind === 'PermissionChange/v1' || category.startsWith('rbac') || type.includes('permission')) return CHANGE_BADGE_STYLES.permission; + if (kind === 'Backup/v1' || category.startsWith('backup') || type.includes('backup') || type.includes('restore')) return CHANGE_BADGE_STYLES.backup; + if (kind === 'CostChange/v1' || type.includes('cost')) return CHANGE_BADGE_STYLES.cost; + if (kind === 'Promotion/v1' || kind === 'Rollback/v1' || kind === 'PipelineRun/v1' || kind === 'PlaybookExecution/v1') return CHANGE_BADGE_STYLES.release; + if (kind === 'Scaling/v1' || type.includes('replica') || type.includes('scaling')) return CHANGE_BADGE_STYLES.scale; + if (kind === 'Deployment/v1' || type === 'diff' || category.startsWith('deployment')) return CHANGE_BADGE_STYLES.diff; + if (type.includes('policy') || normalizedLabel.includes('policy')) return CHANGE_BADGE_STYLES.policy; + return CHANGE_BADGE_STYLES.default; +} + +function getChangeIconName(change: ConfigChange): string { + return change.typedChange?.kind ? change.typedChange.kind.split('/')[0] : change.changeType; +} + +function ChangeIcon({ change }: { change: ConfigChange }) { + return ( + + + + ); +} + +function ChangeTypeBadge({ change, label }: { change: ConfigChange; label: string }) { + const accent = getChangeAccent(change, label); + + return ( + + ); +} + +function SecondaryMeta({ label, className = 'text-gray-500' }: { label: string; className?: string }) { + return ( + + {label} + + ); +} function ChangeEntry({ change, dateFormat, hideConfigName }: { change: ConfigChange; dateFormat: TimeBucketFormat; hideConfigName?: boolean }) { const sev = change.severity ?? 'info'; const author = change.createdBy || change.externalCreatedBy || change.source || ''; const artifactCount = (change.artifacts || []).length; + const typedDisplay = getTypedChangeDisplay(change); + const summary = change.summary || typedDisplay?.summary; + const changeTypeLabel = getChangeTypeLabel(change, typedDisplay); + const hasSecondaryMeta = sev !== 'info' || Boolean(author); + const hasPrimaryMeta = Boolean( + typedDisplay?.diff + || (typedDisplay?.meta && typedDisplay.meta.length > 0) + || (!hideConfigName && change.configName) + || (change.count ?? 0) > 1 + || artifactCount > 0, + ); return ( -
+
{change.createdAt ? formatEntryDate(change.createdAt, dateFormat) : '-'} - - - - {change.changeType} - {!hideConfigName && change.configName && ( - {change.configName} - )} - {change.summary ?? '-'} - {sev !== 'info' && ( - - {sev} - - )} - {author && {author}} - {(change.count ?? 0) > 1 && ( - ×{change.count} - )} - {artifactCount > 0 && ( - - {artifactCount} screenshot{artifactCount > 1 ? 's' : ''} → - - )} + +
+
+
+ + {summary && ( +
+ {summary} +
+ )} +
+ {hasPrimaryMeta && ( +
+ {typedDisplay?.diff && ( + + {typedDisplay.diff.label && ( + + {typedDisplay.diff.label} + + )} + + {typedDisplay.diff.from} + + + + {typedDisplay.diff.to} + + + )} + {!hideConfigName && change.configName && ( + + )} + {typedDisplay?.meta?.map((meta) => ( + + ))} + {(change.count ?? 0) > 1 && ( + + )} + {artifactCount > 0 && ( + + 1 ? 's' : ''}`} + color="bg-purple-50" + textColor="text-purple-700" + borderColor="border-purple-200" + className="shrink-0" + /> + + )} +
+ )} +
+ {hasSecondaryMeta && ( +
+ {sev !== 'info' && ( + + )} + {author && ( + + )} +
+ )} +
); } @@ -103,14 +265,15 @@ export default function ConfigChangesSection({ changes, hideConfigName: hideConf return (
-
+
{SEVERITY_ORDER.filter((sev) => bySeverity[sev] > 0).map((sev) => ( - +
+ +
))}
{groups.map((group) => ( diff --git a/report/components/ConfigInsightsSection.tsx b/report/components/ConfigInsightsSection.tsx index 753cb51d7..9cf80900a 100644 --- a/report/components/ConfigInsightsSection.tsx +++ b/report/components/ConfigInsightsSection.tsx @@ -9,6 +9,7 @@ interface Props { } const SEVERITY_ORDER: ConfigSeverity[] = ['critical', 'high', 'medium', 'low', 'info']; +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; const SEVERITY_COLOR: Record = { critical: 'red', high: 'orange', @@ -97,14 +98,15 @@ export default function ConfigInsightsSection({ analyses }: Props) { return (
-
+
{SEVERITY_ORDER.map((sev) => ( - +
+ +
))}
{ANALYSIS_TYPES.map((type) => ( diff --git a/report/components/DeploymentChanges.tsx b/report/components/DeploymentChanges.tsx index 180b8f881..24758adef 100644 --- a/report/components/DeploymentChanges.tsx +++ b/report/components/DeploymentChanges.tsx @@ -12,6 +12,7 @@ interface Props { } const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; const CATEGORY_STYLES: Record<'scale' | 'policy' | 'spec', string> = { scale: 'bg-blue-50 text-blue-700 border-blue-200', policy: 'bg-orange-50 text-orange-700 border-orange-200', @@ -54,8 +55,8 @@ export default function DeploymentChanges({ changes }: Props) { return ( <> -
-
+
+
-
+
-
+
-
+
= { critical: 'red', @@ -78,14 +79,15 @@ export default function FindingsSection({ findings }: Props) { return (
-
+
{SEVERITY_ORDER.map((sev) => ( - +
+ +
))}
{findings.length === 0 ? ( diff --git a/report/components/IncidentsSection.tsx b/report/components/IncidentsSection.tsx index 3c06ec616..769665aa6 100644 --- a/report/components/IncidentsSection.tsx +++ b/report/components/IncidentsSection.tsx @@ -8,6 +8,7 @@ interface Props { } const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'] as const; +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; const SEVERITY_COLOR: Record = { critical: 'red', high: 'orange', @@ -30,14 +31,15 @@ export default function IncidentsSection({ incidents }: Props) { return (
-
+
{SEVERITY_ORDER.map((sev) => ( - +
+ +
))}
{rows.length > 0 ? ( diff --git a/report/components/RBACChanges.tsx b/report/components/RBACChanges.tsx index 93605add7..e39fd1e57 100644 --- a/report/components/RBACChanges.tsx +++ b/report/components/RBACChanges.tsx @@ -10,6 +10,7 @@ interface Props { } const COUNT_VALUE_CLASS = 'text-[16pt] leading-[18pt]'; +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; const ACTION_BADGE_COLORS: Record<'Granted' | 'Revoked', { color: string; textColor: string; borderColor: string }> = { Granted: { color: 'bg-green-50', @@ -40,8 +41,8 @@ export default function RBACChanges({ changes }: Props) { return ( <> -
-
+
+
-
+
-
+
0 ? `+${netCount}` : String(netCount)} diff --git a/report/components/RBACSummarySection.tsx b/report/components/RBACSummarySection.tsx index 0d5a149cd..5d8b4da55 100644 --- a/report/components/RBACSummarySection.tsx +++ b/report/components/RBACSummarySection.tsx @@ -6,24 +6,38 @@ interface Props { summary: RBACSummary; } +const NO_BREAK_STYLE = { pageBreakInside: 'avoid' as const, breakInside: 'avoid' as const }; + export default function RBACSummarySection({ summary }: Props) { return (
-
- - - - - 0 ? 'orange' : undefined} - /> - 0 ? 'red' : undefined} - /> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ 0 ? 'orange' : undefined} + /> +
+
+ 0 ? 'red' : undefined} + /> +
); diff --git a/report/components/change-section-utils.ts b/report/components/change-section-utils.ts index 660f8df66..f0cd527aa 100644 --- a/report/components/change-section-utils.ts +++ b/report/components/change-section-utils.ts @@ -1,5 +1,6 @@ import type { ApplicationChange, ApplicationPermissionChange } from '../types.ts'; -import type { ConfigChange } from '../config-types.ts'; +import type { ConfigChange, ConfigTypedChange } from '../config-types.ts'; +import type { CatalogReportCategoryMapping } from '../catalog-report-types.ts'; export type ChangeSectionVariant = 'generic' | 'rbac' | 'backup' | 'deployment'; export type BackupCalendarStatus = 'success' | 'failed' | 'warning'; @@ -36,6 +37,26 @@ export interface RBACChangeGroup { rows: RBACChangeRow[]; } +export interface CategorizedChanges { + rbac: ConfigChange[]; + backup: ConfigChange[]; + deployment: ConfigChange[]; + uncategorized: ConfigChange[]; +} + +export interface TypedChangeDisplay { + label?: string; + summary?: string; + meta: string[]; + diff?: TypedChangeDiff; +} + +export interface TypedChangeDiff { + label?: string; + from: string; + to: string; +} + const RBAC_ADDED_TYPES = new Set(['PermissionGranted', 'PermissionAdded']); const RBAC_REMOVED_TYPES = new Set(['PermissionRevoked', 'PermissionRemoved']); const BACKUP_SUCCESS_TYPES = new Set(['BackupCompleted', 'BackupSuccessful']); @@ -56,41 +77,6 @@ function getCategoryKey(change: ApplicationChange): string { return change.category ?? ''; } -type ChangeMappingInput = { - changeType?: string; - severity?: string; - status?: string; -}; - -function getRuleSeverity(rule: string): string | undefined { - const parts = rule.split('@'); - if (parts.length !== 2) { - return undefined; - } - return cleanField(parts[1])?.toLowerCase(); -} - -function getRuleType(rule: string): string { - return rule.split('@', 2)[0]?.trim() ?? ''; -} - -function getChangeSeverity(change: ChangeMappingInput): string { - return (change.status ?? change.severity ?? '').trim().toLowerCase(); -} - -function matchesCategoryRule(change: ChangeMappingInput, rule: string): boolean { - if ((change.changeType ?? '') !== getRuleType(rule)) { - return false; - } - - const ruleSeverity = getRuleSeverity(rule); - if (!ruleSeverity) { - return true; - } - - return getChangeSeverity(change) === ruleSeverity; -} - function normalizeRBACAction(change: ApplicationChange): RBACChangeAction | null { const category = getCategoryKey(change); if (category === 'rbac.granted') { @@ -181,38 +167,10 @@ export function filterDeploymentChanges(changes: ApplicationChange[]): Applicati return changes.filter(isDeploymentChange); } -function resolveCategoryMappings(categoryMappings?: Record): Record | undefined { - if (!categoryMappings || Object.keys(categoryMappings).length === 0) { - return undefined; - } - return categoryMappings; -} - -function findCategoryForChange(change: ChangeMappingInput, categoryMappings?: Record): string | undefined { - const mappings = resolveCategoryMappings(categoryMappings); - if (!mappings) { - return undefined; - } - - for (const [category, rules] of Object.entries(mappings)) { - if (rules.some((rule) => getRuleSeverity(rule) && matchesCategoryRule(change, rule))) { - return category; - } - } - - for (const [category, types] of Object.entries(mappings)) { - if (types.some((rule) => !getRuleSeverity(rule) && matchesCategoryRule(change, rule))) { - return category; - } - } - - return undefined; -} - export function inferChangeSectionVariant( title: string, changes: ApplicationChange[], - categoryMappings?: Record, + _categoryMappings?: CatalogReportCategoryMapping[], ): ChangeSectionVariant { const lowerTitle = title.toLowerCase(); @@ -228,36 +186,34 @@ export function inferChangeSectionVariant( return 'deployment'; } - const mappings = resolveCategoryMappings(categoryMappings); - if (mappings) { - let rbacCount = 0; - let backupCount = 0; - let deploymentCount = 0; - for (const change of changes) { - const category = findCategoryForChange(change, mappings); - if (!category) { - continue; - } - if (category === 'rbac' || category.startsWith('rbac.')) { - rbacCount += 1; - } else if (category === 'backup' || category.startsWith('backup.')) { - backupCount += 1; - } else if (category === 'deployment' || category.startsWith('deployment.')) { - deploymentCount += 1; - } + let rbacCount = 0; + let backupCount = 0; + let deploymentCount = 0; + for (const change of changes) { + const category = getCategoryKey(change); + if (!category) { + continue; } - if (rbacCount > 0 && rbacCount === changes.length) { - return 'rbac'; - } - if (backupCount > 0 && backupCount === changes.length) { - return 'backup'; - } - if (deploymentCount > 0 && deploymentCount >= Math.ceil(changes.length / 2)) { - return 'deployment'; + if (category === 'rbac' || category.startsWith('rbac.')) { + rbacCount += 1; + } else if (category === 'backup' || category.startsWith('backup.')) { + backupCount += 1; + } else if (category === 'deployment' || category.startsWith('deployment.')) { + deploymentCount += 1; } } + if (rbacCount > 0 && rbacCount === changes.length) { + return 'rbac'; + } + if (backupCount > 0 && backupCount === changes.length) { + return 'backup'; + } + if (deploymentCount > 0 && deploymentCount >= Math.ceil(changes.length / 2)) { + return 'deployment'; + } + return 'generic'; } @@ -488,46 +444,304 @@ export function groupRBACChanges(changes: ApplicationChange[]): RBACChangeGroup[ .sort((a, b) => new Date(b.latestDate).getTime() - new Date(a.latestDate).getTime()); } -export interface CategorizedChanges { - rbac: Array<{ change: ConfigChange; category: string }>; - backup: Array<{ change: ConfigChange; category: string }>; - deployment: Array<{ change: ConfigChange; category: string }>; - uncategorized: ConfigChange[]; -} - export function categorizeChanges( changes: ConfigChange[], - categoryMappings?: Record, + _categoryMappings?: CatalogReportCategoryMapping[], ): CategorizedChanges { const result: CategorizedChanges = { rbac: [], backup: [], deployment: [], uncategorized: [] }; - const mappings = resolveCategoryMappings(categoryMappings); - if (!mappings) { - result.uncategorized = changes; - return result; - } for (const change of changes) { - const category = findCategoryForChange(change, mappings); + const category = change.category ?? ''; if (!category) { result.uncategorized.push(change); continue; } - if (category === 'rbac' || category.startsWith('rbac.')) result.rbac.push({ change, category }); - else if (category === 'backup' || category.startsWith('backup.')) result.backup.push({ change, category }); - else if (category === 'deployment' || category.startsWith('deployment.')) result.deployment.push({ change, category }); + if (category === 'rbac' || category.startsWith('rbac.')) result.rbac.push(change); + else if (category === 'backup' || category.startsWith('backup.')) result.backup.push(change); + else if (category === 'deployment' || category.startsWith('deployment.')) result.deployment.push(change); else result.uncategorized.push(change); } + return result; } +function asText(value: unknown): string | undefined { + if (value === undefined || value === null) { + return undefined; + } + if (typeof value === 'string') { + return cleanField(value); + } + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + return undefined; +} + +function compactMeta(values: Array): string[] { + return values.filter((value): value is string => Boolean(value)); +} + +function labelValue(label: string, value: unknown): string | undefined { + const text = asText(value); + return text ? `${label}: ${text}` : undefined; +} + +function transition(label: string, from: unknown, to: unknown): string | undefined { + const fromText = asText(from); + const toText = asText(to); + if (!fromText && !toText) { + return undefined; + } + if (!fromText) { + return `${label}: ${toText}`; + } + if (!toText) { + return `${label}: ${fromText}`; + } + return `${label}: ${fromText} -> ${toText}`; +} + +function toDiff(label: string, from: unknown, to: unknown): TypedChangeDiff | undefined { + const fromText = asText(from); + const toText = asText(to); + if (!fromText || !toText || fromText === toText) { + return undefined; + } + + return { label, from: fromText, to: toText }; +} + +function formatDimensions(width: unknown, height: unknown): string | undefined { + const widthText = asText(width); + const heightText = asText(height); + if (!widthText || !heightText) { + return undefined; + } + return `${widthText}x${heightText}`; +} + +function formatCurrencyAmount(value: unknown, currency: unknown): string | undefined { + if (typeof value !== 'number') { + return asText(value); + } + + const code = asText(currency)?.toUpperCase(); + if (code && code.length === 3) { + try { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: code }).format(value); + } catch { + return `${code} ${value}`; + } + } + + return String(value); +} + +function humanizeLabel(value: string): string { + return value + .replace(/[_-]+/g, ' ') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/\s+/g, ' ') + .trim() + .replace(/^./, (char) => char.toUpperCase()); +} + +function humanizeKind(kind: string): string { + const base = kind.split('/')[0] ?? kind; + return humanizeLabel(base); +} + +export function getChangeTypeLabel(change: ConfigChange, typedDisplay?: TypedChangeDisplay): string { + const typeLabel = humanizeLabel(change.changeType || 'Change'); + const normalizedType = (change.changeType || '').trim().toLowerCase(); + + if (typedDisplay?.label && ['diff', 'change', 'changed', 'update', 'updated'].includes(normalizedType)) { + return typedDisplay.label; + } + + return typeLabel; +} + +function permissionFromTypedChange(typedChange?: ConfigTypedChange): ApplicationPermissionChange | undefined { + if (typedChange?.kind !== 'PermissionChange/v1') { + return undefined; + } + + const user = asText(typedChange.user_name); + const group = asText(typedChange.group_name); + const role = asText(typedChange.role_name); + if (!user && !group && !role) { + return undefined; + } + + return { user, group, role }; +} + +const TYPED_CHANGE_RENDERERS: Record Omit> = { + 'UserChange/v1': (typedChange) => ({ + meta: compactMeta([ + asText(typedChange.user_name) || asText(typedChange.user_id), + asText(typedChange.user_email), + labelValue('Group', typedChange.group_name || typedChange.group_id), + labelValue('Type', typedChange.user_type), + labelValue('Tenant', typedChange.tenant), + ]), + }), + 'Screenshot/v1': (typedChange) => ({ + meta: compactMeta([ + labelValue('Artifact', typedChange.artifact_id), + labelValue('Type', typedChange.content_type), + labelValue('Size', formatDimensions(typedChange.width, typedChange.height)), + labelValue('URL', typedChange.url), + ]), + }), + 'PermissionChange/v1': (typedChange) => ({ + meta: compactMeta([ + asText(typedChange.user_name) || asText(typedChange.group_name) || asText(typedChange.user_id) || asText(typedChange.group_id), + labelValue('Role', typedChange.role_name || typedChange.role_id), + labelValue('Role Type', typedChange.role_type), + labelValue('Scope', typedChange.scope), + ]), + }), + 'Deployment/v1': (typedChange) => { + const imageDiff = toDiff('Image', typedChange.previous_image, typedChange.new_image); + return { + meta: compactMeta([ + labelValue('Container', typedChange.container), + imageDiff ? undefined : transition('Image', typedChange.previous_image, typedChange.new_image), + labelValue('Namespace', typedChange.namespace), + labelValue('Strategy', typedChange.strategy), + ]), + diff: imageDiff, + }; + }, + 'Promotion/v1': (typedChange) => { + const environmentDiff = toDiff('Environment', typedChange.from_environment, typedChange.to_environment); + return { + meta: compactMeta([ + environmentDiff ? undefined : transition('Environment', typedChange.from_environment, typedChange.to_environment), + labelValue('Version', typedChange.version), + labelValue('Artifact', typedChange.artifact), + ]), + diff: environmentDiff, + }; + }, + 'Approval/v1': (typedChange) => ({ + summary: asText(typedChange.approved_by) ? `Approved by ${typedChange.approved_by}` : asText(typedChange.rejected_by) ? `Rejected by ${typedChange.rejected_by}` : 'Approval decision', + meta: compactMeta([ + labelValue('Playbook', typedChange.playbook_id), + labelValue('Run', typedChange.run_id), + labelValue('Reason', typedChange.reason), + ]), + }), + 'Rollback/v1': (typedChange) => { + const versionDiff = toDiff('Version', typedChange.from_version, typedChange.to_version); + return { + summary: labelValue('Reason', typedChange.reason), + meta: compactMeta([ + versionDiff ? undefined : transition('Version', typedChange.from_version, typedChange.to_version), + labelValue('Trigger', typedChange.trigger), + ]), + diff: versionDiff, + }; + }, + 'Backup/v1': (typedChange) => ({ + meta: compactMeta([ + labelValue('Status', typedChange.status), + labelValue('Type', typedChange.backup_type), + labelValue('Target', typedChange.target), + labelValue('Size', typedChange.size), + labelValue('Duration', typedChange.duration), + labelValue('Snapshot', typedChange.snapshot_id), + ]), + }), + 'PlaybookExecution/v1': (typedChange) => { + const playbook = asText(typedChange.playbook_name) || asText(typedChange.playbook_id); + return { + summary: playbook, + meta: compactMeta([ + labelValue('Run', typedChange.run_id), + labelValue('Status', typedChange.status), + labelValue('Duration', typedChange.duration), + labelValue('Error', typedChange.error), + ]), + }; + }, + 'Scaling/v1': (typedChange) => { + const replicaDiff = toDiff('Replicas', typedChange.from_replicas, typedChange.to_replicas); + return { + meta: compactMeta([ + labelValue('Resource', typedChange.resource_type), + replicaDiff ? undefined : transition('Replicas', typedChange.from_replicas, typedChange.to_replicas), + labelValue('Trigger', typedChange.trigger), + ]), + diff: replicaDiff, + }; + }, + 'Certificate/v1': (typedChange) => ({ + summary: labelValue('Subject', typedChange.subject), + meta: compactMeta([ + labelValue('Issuer', typedChange.issuer), + labelValue('Valid To', typedChange.not_after), + labelValue('Serial', typedChange.serial), + labelValue('DNS', typedChange.dns_names), + ]), + }), + 'CostChange/v1': (typedChange) => { + const costDiff = toDiff( + 'Cost', + formatCurrencyAmount(typedChange.previous_cost, typedChange.currency), + formatCurrencyAmount(typedChange.new_cost, typedChange.currency), + ); + return { + summary: labelValue('Reason', typedChange.reason), + meta: compactMeta([ + costDiff ? undefined : transition('Cost', formatCurrencyAmount(typedChange.previous_cost, typedChange.currency), formatCurrencyAmount(typedChange.new_cost, typedChange.currency)), + labelValue('Period', typedChange.period), + ]), + diff: costDiff, + }; + }, + 'PipelineRun/v1': (typedChange) => { + const pipeline = asText(typedChange.pipeline_name) || asText(typedChange.pipeline_id); + return { + summary: pipeline, + meta: compactMeta([ + labelValue('Run', typedChange.run_number ?? typedChange.run_id), + labelValue('Branch', typedChange.branch), + labelValue('Status', typedChange.status), + labelValue('Duration', typedChange.duration), + labelValue('Error', typedChange.error), + ]), + }; + }, +}; + +export function getTypedChangeDisplay(change: ConfigChange): TypedChangeDisplay | undefined { + const typedChange = change.typedChange; + if (!typedChange?.kind) { + return undefined; + } + + const renderer = TYPED_CHANGE_RENDERERS[typedChange.kind]; + const display = renderer ? renderer(typedChange) : { meta: [] }; + return { + label: humanizeKind(typedChange.kind), + summary: display.summary, + meta: display.meta ?? [], + diff: display.diff, + }; +} + export function configChangeToApplicationChange(c: ConfigChange, category?: string): ApplicationChange { - const permission = c.details?.permission as ApplicationPermissionChange | undefined; + const permission = (c.details?.permission as ApplicationPermissionChange | undefined) ?? permissionFromTypedChange(c.typedChange); return { id: c.id ?? '', date: c.createdAt ?? '', changeType: c.changeType, - category, + category: category ?? c.category, source: c.source, createdBy: c.createdBy ?? c.externalCreatedBy, configId: c.configID, diff --git a/report/config-types.ts b/report/config-types.ts index 33343b4d8..5fb529a57 100644 --- a/report/config-types.ts +++ b/report/config-types.ts @@ -28,6 +28,11 @@ export interface ConfigChangeArtifact { dataUri?: string; } +export interface ConfigTypedChange { + kind: string; + [key: string]: any; +} + export interface ConfigChange { id?: string; configID?: string; @@ -35,10 +40,12 @@ export interface ConfigChange { configType?: string; permalink?: string; changeType: string; + category?: string; severity?: ConfigSeverity; source?: string; summary?: string; details?: Record; + typedChange?: ConfigTypedChange; createdBy?: string; externalCreatedBy?: string; createdAt?: string; diff --git a/report/kitchen-sink-data.ts b/report/kitchen-sink-data.ts index b63d0d6b2..1779c058b 100644 --- a/report/kitchen-sink-data.ts +++ b/report/kitchen-sink-data.ts @@ -6,6 +6,10 @@ import type { KitchenSinkData } from './kitchen-sink/KitchenSinkTypes.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const raw = readFileSync(resolve(__dirname, 'testdata/kitchen-sink.yaml'), 'utf-8'); -const data = yaml.load(raw) as KitchenSinkData; +export const data = yaml.load(raw) as KitchenSinkData; export default data; + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + process.stdout.write(JSON.stringify(data)); +} diff --git a/report/kitchen-sink/ChangesPage.tsx b/report/kitchen-sink/ChangesPage.tsx index 2a6c9fa9d..bd14ef0a3 100644 --- a/report/kitchen-sink/ChangesPage.tsx +++ b/report/kitchen-sink/ChangesPage.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { Page, Section } from '@flanksource/facet'; import type { KitchenSinkData } from './KitchenSinkTypes.ts'; +import type { CatalogReportCategoryMapping } from '../catalog-report-types.ts'; import ConfigChangesSection from '../components/ConfigChangesSection.tsx'; +import ConfigChangesExamples from '../components/ConfigChangesExamples.tsx'; import RBACChanges from '../components/RBACChanges.tsx'; import BackupChanges from '../components/BackupChanges.tsx'; import DeploymentChanges from '../components/DeploymentChanges.tsx'; @@ -16,28 +18,30 @@ export default function ChangesPage({ data, pageProps }: Props) { const rbacChanges = data.rbacChanges ?? []; const backupChanges = data.backupChanges ?? []; const deploymentChanges = data.deploymentChanges ?? []; - const categoryMappings = (data as any).categoryMappings as Record | undefined; + const categoryMappings = (data as any).categoryMappings as CatalogReportCategoryMapping[] | undefined; const categorized = categorizeChanges(data.changes ?? [], categoryMappings); return ( + +
A single changes array auto-split into specialized sections using categoryMappings. RBAC, backup, and deployment changes get their own renderers; the rest falls through to ConfigChangesSection.
{categorized.rbac.length > 0 && (
- configChangeToApplicationChange(change, category))} /> + configChangeToApplicationChange(change))} />
)} {categorized.backup.length > 0 && (
- configChangeToApplicationChange(change, category))} /> + configChangeToApplicationChange(change))} />
)} {categorized.deployment.length > 0 && (
- configChangeToApplicationChange(change, category))} /> + configChangeToApplicationChange(change))} />
)} {categorized.uncategorized.length > 0 && ( diff --git a/report/mission-control.ts b/report/mission-control.ts index 46a63014f..ee1bda842 100644 --- a/report/mission-control.ts +++ b/report/mission-control.ts @@ -6,6 +6,10 @@ import type { Application } from './types.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); const raw = readFileSync(resolve(__dirname, 'fixtures/mission-control.yaml'), 'utf-8'); -const data = yaml.load(raw) as Application; +export const data = yaml.load(raw) as Application; export default data; + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + process.stdout.write(JSON.stringify(data)); +} diff --git a/report/testdata/kitchen-sink.yaml b/report/testdata/kitchen-sink.yaml index 7f356d62d..3143ff31e 100644 --- a/report/testdata/kitchen-sink.yaml +++ b/report/testdata/kitchen-sink.yaml @@ -15,37 +15,30 @@ configItem: updatedAt: "2026-03-28T12:00:00Z" categoryMappings: - rbac.granted: - - PermissionGranted - - PermissionAdded - - IAMRoleAdded - rbac.revoked: - - PermissionRevoked - - PermissionRemoved - - IAMRoleRemoved - backup.success: - - BackupCompleted - - BackupSuccessful - backup.failed: - - BackupFailed - backup.progress: - - BackupStarted - - BackupRunning - - BackupEnqueued - backup.restore: - - BackupRestored - - RestoreCompleted - deployment.spec: - - diff - deployment.scale: - - ScalingReplicaSet - deployment.policy: - - PolicyUpdate + - category: rbac.granted + filter: 'changeType == "PermissionGranted" || changeType == "PermissionAdded" || changeType == "IAMRoleAdded"' + - category: rbac.revoked + filter: 'changeType == "PermissionRevoked" || changeType == "PermissionRemoved" || changeType == "IAMRoleRemoved"' + - category: backup.success + filter: 'changeType == "BackupCompleted" || changeType == "BackupSuccessful"' + - category: backup.failed + filter: 'changeType == "BackupFailed"' + - category: backup.progress + filter: 'changeType == "BackupStarted" || changeType == "BackupRunning" || changeType == "BackupEnqueued"' + - category: backup.restore + filter: 'changeType == "BackupRestored" || changeType == "RestoreCompleted"' + - category: deployment.spec + filter: 'changeType == "diff"' + - category: deployment.scale + filter: 'changeType == "ScalingReplicaSet"' + - category: deployment.policy + filter: 'changeType == "PolicyUpdate"' changes: - id: "chg-001" configID: "cfg-eks-001" changeType: "diff" + category: "deployment.spec" severity: "info" source: "kubernetes" summary: "Node pool autoscaler adjusted desired count from 3 to 5" @@ -65,6 +58,7 @@ changes: - id: "chg-003" configID: "cfg-eks-001" changeType: "ScalingReplicaSet" + category: "deployment.scale" severity: "low" source: "kubernetes" summary: "Deployment incident-commander scaled from 2 to 3 replicas" @@ -74,6 +68,7 @@ changes: - id: "chg-004" configID: "cfg-eks-001" changeType: "diff" + category: "deployment.spec" severity: "medium" source: "terraform" summary: "EKS cluster version upgraded from 1.28 to 1.29" @@ -83,6 +78,7 @@ changes: - id: "chg-005" configID: "cfg-eks-001" changeType: "PolicyUpdate" + category: "deployment.policy" severity: "high" source: "argocd" summary: "Network policy updated: restricted egress to 10.0.0.0/8 for namespace mc" @@ -92,6 +88,7 @@ changes: - id: "chg-006" configID: "cfg-eks-001" changeType: "diff" + category: "deployment.spec" severity: "critical" source: "aws-config" summary: "IAM role policy detached: eks-admin-access removed from cluster role" @@ -119,6 +116,7 @@ changes: - id: "chg-009" configID: "cfg-eks-001" changeType: "ScalingReplicaSet" + category: "deployment.scale" severity: "info" source: "kubernetes" summary: "Deployment canary-checker scaled from 1 to 2 replicas" @@ -128,6 +126,7 @@ changes: - id: "chg-010" configID: "cfg-eks-001" changeType: "diff" + category: "deployment.spec" severity: "medium" source: "argocd" summary: "Helm release cert-manager upgraded from v1.13.3 to v1.14.1" @@ -146,6 +145,7 @@ changes: - id: "chg-012" configID: "cfg-eks-001" changeType: "PolicyUpdate" + category: "deployment.policy" severity: "high" source: "aws-config" summary: "Security group sg-0abc123 ingress rule added: allow 443 from 0.0.0.0/0" @@ -155,6 +155,7 @@ changes: - id: "chg-013" configID: "cfg-eks-001" changeType: "diff" + category: "deployment.spec" severity: "low" source: "kubernetes" summary: "PodDisruptionBudget added for incident-commander (minAvailable: 2)" @@ -164,6 +165,7 @@ changes: - id: "chg-014" configID: "cfg-eks-001" changeType: "PermissionGranted" + category: "rbac.granted" severity: "info" source: "okta" summary: "Granted db_owner to alice@flanksource.com on prod-rds-01" @@ -177,6 +179,7 @@ changes: - id: "chg-015" configID: "cfg-eks-001" changeType: "PermissionRevoked" + category: "rbac.revoked" severity: "info" source: "okta" summary: "Revoked Secrets Reader access for bob@flanksource.com on prod-eks-cluster" @@ -190,6 +193,7 @@ changes: - id: "chg-016" configID: "cfg-eks-001" changeType: "BackupCompleted" + category: "backup.success" severity: "info" source: "velero" summary: "Full cluster backup completed successfully (2.4 GiB)" @@ -198,6 +202,7 @@ changes: - id: "chg-017" configID: "cfg-eks-001" changeType: "BackupFailed" + category: "backup.failed" severity: "high" source: "velero" summary: "Incremental backup failed: PVC snapshot timeout after 300s" @@ -206,11 +211,188 @@ changes: - id: "chg-018" configID: "cfg-eks-001" changeType: "BackupStarted" + category: "backup.progress" severity: "info" source: "velero" summary: "Scheduled backup initiated for prod-eks-cluster" createdAt: "2026-03-30T03:00:00Z" + - id: "chg-019" + configID: "cfg-eks-001" + changeType: "UserCreated" + severity: "info" + source: "okta" + createdAt: "2026-03-24T11:30:00Z" + typedChange: + kind: "UserChange/v1" + user_name: "alice" + user_email: "alice@flanksource.com" + user_type: "human" + group_name: "platform-admins" + tenant: "production" + + - id: "chg-020" + configID: "cfg-eks-001" + changeType: "Screenshot" + severity: "info" + source: "synthetics" + createdAt: "2026-03-24T10:15:00Z" + typedChange: + kind: "Screenshot/v1" + artifact_id: "art-001" + content_type: "image/png" + width: 1440 + height: 900 + url: "https://prod-eks-cluster.example.com/login" + + - id: "chg-021" + configID: "cfg-eks-001" + changeType: "PermissionSync" + severity: "low" + source: "iam-reconciler" + createdAt: "2026-03-24T09:45:00Z" + typedChange: + kind: "PermissionChange/v1" + user_name: "jane@flanksource.com" + role_name: "cluster-admin" + scope: "namespace/mc" + + - id: "chg-022" + configID: "cfg-eks-001" + changeType: "Deployment" + severity: "info" + source: "argocd" + createdAt: "2026-03-24T09:00:00Z" + typedChange: + kind: "Deployment/v1" + previous_image: "flanksource/incident-commander:v1.4.190" + new_image: "flanksource/incident-commander:v1.4.200" + container: "incident-commander" + namespace: "mc" + strategy: "rolling" + + - id: "chg-023" + configID: "cfg-eks-001" + changeType: "Promotion" + severity: "info" + source: "release-bot" + createdAt: "2026-03-24T08:30:00Z" + typedChange: + kind: "Promotion/v1" + from_environment: "staging" + to_environment: "production" + version: "v1.4.200" + artifact: "incident-commander" + + - id: "chg-024" + configID: "cfg-eks-001" + changeType: "Approved" + severity: "info" + source: "playbooks" + createdAt: "2026-03-24T08:00:00Z" + typedChange: + kind: "Approval/v1" + playbook_id: "pb-001" + run_id: "run-approve-001" + approved_by: "ops-lead@flanksource.com" + reason: "Change window approved" + + - id: "chg-025" + configID: "cfg-eks-001" + changeType: "Rollback" + severity: "high" + source: "argocd" + createdAt: "2026-03-24T07:30:00Z" + typedChange: + kind: "Rollback/v1" + from_version: "v1.4.200" + to_version: "v1.4.190" + trigger: "health-check" + reason: "Elevated error rate" + + - id: "chg-026" + configID: "cfg-eks-001" + changeType: "BackupArchived" + severity: "info" + source: "velero" + createdAt: "2026-03-24T07:00:00Z" + typedChange: + kind: "Backup/v1" + status: "completed" + backup_type: "full" + size: "2.4 GiB" + duration: "4m12s" + target: "s3://velero-prod" + snapshot_id: "snap-019" + + - id: "chg-027" + configID: "cfg-eks-001" + changeType: "PlaybookCompleted" + severity: "info" + source: "playbooks" + createdAt: "2026-03-24T06:30:00Z" + typedChange: + kind: "PlaybookExecution/v1" + playbook_name: "Restart Incident Commander" + run_id: "pb-run-019" + status: "completed" + duration: "2m11s" + + - id: "chg-028" + configID: "cfg-eks-001" + changeType: "Scaling" + severity: "low" + source: "keda" + createdAt: "2026-03-24T06:00:00Z" + typedChange: + kind: "Scaling/v1" + from_replicas: 2 + to_replicas: 4 + resource_type: "Deployment" + trigger: "queue-depth" + + - id: "chg-029" + configID: "cfg-eks-001" + changeType: "CertificateRenewed" + severity: "info" + source: "cert-manager" + createdAt: "2026-03-24T05:30:00Z" + typedChange: + kind: "Certificate/v1" + subject: "prod-eks-cluster.internal" + issuer: "letsencrypt-prod" + not_after: "2026-06-22T00:00:00Z" + serial: "09AF23" + dns_names: "prod-eks-cluster.internal,api.prod.example.com" + + - id: "chg-030" + configID: "cfg-eks-001" + changeType: "CostChange" + severity: "medium" + source: "cost-analyzer" + createdAt: "2026-03-24T05:00:00Z" + typedChange: + kind: "CostChange/v1" + previous_cost: 4280.5 + new_cost: 4631.25 + currency: "USD" + period: "30d" + reason: "Node group scale-out" + + - id: "chg-031" + configID: "cfg-eks-001" + changeType: "PipelineRunCompleted" + severity: "info" + source: "github-actions" + createdAt: "2026-03-24T04:30:00Z" + typedChange: + kind: "PipelineRun/v1" + pipeline_name: "deploy-incident-commander" + run_number: 841 + branch: "main" + status: "completed" + duration: "9m31s" + analyses: - id: "ana-001" configID: "cfg-eks-001" From 353b3fa6dc84f6bee8227a164e554e4c68e6a1a8 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 10 Apr 2026 13:14:37 +0300 Subject: [PATCH 34/48] feat(report): add audit findings report and group membership visualization Introduces FindingsReport component for rendering security audit findings with severity-based styling, evidence tables, and data source tracking. Adds group membership audit section showing user assignments and timeline. Includes comprehensive icon set for attack categories, kill chain phases, and identity types. --- report/FindingsReport.tsx | 571 ++++++++++++++++++ report/catalog-report-types.ts | 18 + report/components/AuditPage.tsx | 48 +- .../components/CatalogAccessLogsSection.tsx | 8 +- report/components/CatalogAccessSection.tsx | 8 +- report/components/ConfigChangesSection.tsx | 194 +++--- report/components/ConfigInsightsSection.tsx | 16 +- report/components/ConfigItemCard.tsx | 16 +- report/components/CoverPage.tsx | 20 +- report/components/DynamicSection.tsx | 44 +- report/components/GitRef.tsx | 14 +- report/components/RBACChangelogSection.tsx | 18 +- report/components/RBACMatrixSection.tsx | 17 +- report/components/RBACUserSection.tsx | 18 +- report/components/ScraperCard.tsx | 27 +- report/components/ViewResultSection.tsx | 46 +- report/icons.ts | 219 +++++++ report/kitchen-sink/CatalogPage.tsx | 12 +- report/package.json | 9 + report/testdata/kitchen-sink.yaml | 74 +++ 20 files changed, 1213 insertions(+), 184 deletions(-) create mode 100644 report/FindingsReport.tsx create mode 100644 report/icons.ts diff --git a/report/FindingsReport.tsx b/report/FindingsReport.tsx new file mode 100644 index 000000000..4552a9251 --- /dev/null +++ b/report/FindingsReport.tsx @@ -0,0 +1,571 @@ +import React from "react"; +import { Document, Page, Header, Footer, SeverityStatCard, ListTable, Badge as FacetBadge, PageNo } from "@flanksource/facet"; +import { OUTCOME_ICONS, CATEGORY_ICONS, KILL_CHAIN_ICONS, IDENTITY_ICONS, ENDPOINT_ICONS, RESOURCE_ICONS, APP_ICONS, type IconDef } from "./icons"; +import { Sqlserver, K8S, Aws, Azure, MissionControl, MissionControlLogo } from "@flanksource/icons/mi"; +import { Icon } from "@flanksource/icons/icon"; +import vscodeIcons from "@iconify-json/vscode-icons/icons.json"; + +function VscodeIcon({ name, size = 20 }: { name: string; size?: number }) { + const iconName = name.replace("vscode-icons:", ""); + const iconData = (vscodeIcons as any).icons[iconName]; + if (!iconData) return null; + const w = iconData.width || (vscodeIcons as any).width || 32; + const h = iconData.height || (vscodeIcons as any).height || 32; + return ; +} + + +function SvgIcon({ icon, size = 14 }: { icon: IconDef; size?: number }) { + return ; +} + +function svgIconComponent(icon: IconDef): React.ComponentType<{ className?: string }> { + return ({ className }: { className?: string }) => ( + + ); +} + +type Severity = "critical" | "high" | "medium" | "low" | "info"; +type Platform = "sql-server" | "kubernetes" | "aws" | "azure" | "mission-control"; +type Outcome = "safety-switch" | "page-oncall" | "high-ticket" | "low-ticket" | "informational"; +interface Identity { name: string; type: string; displayName?: string } +interface Endpoint { ip?: string; hostname?: string; type?: string; network?: string; tags?: string[] } +interface AppRef { name: string; type?: string; tags?: string[] } +interface Resource { name: string; type: string; scope?: string; tags?: string[] } +interface Actor { identity?: Identity; endpoint?: Endpoint; app?: AppRef; resource?: Resource } +interface AuditSample { timestamp: string; action: string; detail?: string; succeeded?: boolean; src?: Actor; dst?: Actor } +interface FileInfo { + name?: string; size?: string; created?: string; modified?: string; + location?: string; host?: string; +} +interface DataSource { + type?: string; categories?: string[]; connection?: string; path?: string; query?: string; + timeRange?: { start: string; end: string; durationSeconds?: number }; + git?: { sha?: string; repo?: string; file?: string; lineNo?: number; branch?: string; tag?: string }; + contentSha?: string; + app?: { name?: string; version?: string; icon?: string }; + file?: FileInfo; +} +interface AuditFinding { + title: string; severity: Severity; platform: Platform; category: string; outcome: Outcome; + detection: { pattern: string; threshold?: string }; + dataSource?: DataSource; + evidence: { + summary: string; + timeRange?: { start: string; end: string; durationSeconds?: number }; + metrics?: Record; + samples?: AuditSample[]; + }; + recommendation: { action: string; mitigations?: string[]; references?: string[] }; + context?: { + killChainPhase?: string; mitreTechnique?: string; compliance?: string[]; + relatedFindings?: string[]; + baseline?: { normalValue?: number; observedValue?: number; deviationFactor?: number; baselinePeriod?: string }; + }; + provenance?: { generatedAt?: string; generatedBy?: string; version?: string; runId?: string; model?: string }; +} + +const SEVERITY_STYLES: Record = { + critical: { className: "bg-red-50 text-red-700", dot: "bg-red-500", border: "border-red-200", order: 0, color: "red" }, + high: { className: "bg-orange-50 text-orange-700", dot: "bg-orange-500", border: "border-orange-200", order: 1, color: "orange" }, + medium: { className: "bg-yellow-50 text-yellow-700", dot: "bg-yellow-500", border: "border-yellow-200", order: 2, color: "yellow" }, + low: { className: "bg-blue-50 text-blue-700", dot: "bg-blue-400", border: "border-blue-200", order: 3, color: "blue" }, + info: { className: "bg-gray-50 text-gray-600", dot: "bg-gray-400", border: "border-gray-200", order: 4, color: "gray" }, +}; + +const OUTCOME_STYLES: Record; label: string }> = { + "safety-switch": { className: "bg-red-900/10 text-red-900 border border-red-900/20", icon: svgIconComponent(OUTCOME_ICONS["safety-switch"]), label: "Kill Switch" }, + "page-oncall": { className: "bg-red-50 text-red-500 border border-red-200", icon: svgIconComponent(OUTCOME_ICONS["page-oncall"]), label: "Page On-Call" }, + "high-ticket": { className: "bg-orange-50 text-orange-600 border border-orange-200", icon: svgIconComponent(OUTCOME_ICONS["high-ticket"]), label: "High Priority" }, + "low-ticket": { className: "bg-yellow-50 text-yellow-600 border border-yellow-200", icon: svgIconComponent(OUTCOME_ICONS["low-ticket"]), label: "Track Issue" }, + "informational": { className: "bg-gray-50 text-gray-500 border border-gray-200", icon: svgIconComponent(OUTCOME_ICONS["informational"]), label: "Log Only" }, +}; +const PLATFORM_LABELS: Record = { + "sql-server": "SQL Server", kubernetes: "Kubernetes", aws: "AWS", azure: "Azure", "mission-control": "Mission Control", +}; +const PLATFORM_ICONS: Record> = { + "sql-server": Sqlserver, kubernetes: K8S, aws: Aws, azure: Azure, "mission-control": MissionControl, +}; + +function formatDateTime(iso: string): string { + return new Date(iso).toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); +} +function formatDuration(seconds: number): string { + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.round(seconds / 60)}m`; + if (seconds < 86400) return `${Math.round(seconds / 3600)}h`; + return `${Math.round(seconds / 86400)}d`; +} +function formatKey(key: string): string { + return key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()); +} + +interface BadgeProps { label?: string; icon?: React.ComponentType<{ className?: string }>; className?: string; size?: "xs" | "sm" } + +function Badge({ label, icon: Icon, className = "bg-gray-100 text-gray-500", size = "sm" }: BadgeProps) { + return ( + + {Icon && } + {label && {label}} + + ); +} + +function severityBadge(s: Severity) { + return { label: s, className: SEVERITY_STYLES[s].className, dot: SEVERITY_STYLES[s].dot }; +} + +const CATEGORY_ICON_COMPONENTS: Record> = Object.fromEntries( + Object.entries(CATEGORY_ICONS).map(([k, v]) => [k, svgIconComponent(v)]) +); + +function findingSubtitleTags(f: AuditFinding): BadgeProps[] { + const tags: BadgeProps[] = [ + { label: f.category, className: "bg-gray-100 text-gray-500", icon: CATEGORY_ICON_COMPONENTS[f.category] }, + { label: PLATFORM_LABELS[f.platform], className: "bg-gray-100 text-gray-500", icon: PLATFORM_ICONS[f.platform] }, + ]; + if (f.context?.killChainPhase) { + const kcIcon = KILL_CHAIN_ICONS[f.context.killChainPhase]; + tags.push({ label: f.context.killChainPhase, className: "bg-purple-50 text-purple-600", icon: kcIcon ? svgIconComponent(kcIcon) : undefined }); + } + if (f.context?.mitreTechnique) tags.push({ label: `MITRE ${f.context.mitreTechnique}`, className: "bg-purple-50 text-purple-600" }); + return tags; +} + +function findingComplianceTags(f: AuditFinding): BadgeProps[] { + return [ + ...(f.context?.compliance?.map((c) => ({ label: c, className: "bg-gray-50 text-gray-500" })) || []), + ...(f.context?.relatedFindings?.map((r) => ({ label: `→ ${r}`, className: "font-mono bg-gray-50 text-gray-500" })) || []), + ]; +} + +type EntityEntry = { name: string; type: string; scope?: string; className?: string; icon?: React.ComponentType<{ className?: string }> }; + +function iconFor(map: Record, key: string): React.ComponentType<{ className?: string }> | undefined { + const def = map[key]; + return def ? svgIconComponent(def) : undefined; +} +function findingEntities(f: AuditFinding): EntityEntry[] { + const seen = new Set(); + const entities: EntityEntry[] = []; + for (const s of f.evidence.samples || []) { + for (const actor of [s.src, s.dst].filter(Boolean) as Actor[]) { + if (actor.identity && !seen.has(actor.identity.name)) { + seen.add(actor.identity.name); + entities.push({ name: actor.identity.displayName || actor.identity.name, type: actor.identity.type, className: "font-mono bg-gray-100 text-gray-700", icon: iconFor(IDENTITY_ICONS, actor.identity.type) }); + } + if (actor.endpoint?.ip && !seen.has(actor.endpoint.ip)) { + seen.add(actor.endpoint.ip); + entities.push({ name: actor.endpoint.ip, type: actor.endpoint.type || "ip", className: "font-mono bg-gray-100 text-gray-700", icon: iconFor(ENDPOINT_ICONS, actor.endpoint.type || "ip") }); + } + if (actor.app && !seen.has(actor.app.name)) { + seen.add(actor.app.name); + entities.push({ name: actor.app.name, type: actor.app.type || "app", className: "bg-indigo-50 text-indigo-700", icon: iconFor(APP_ICONS, actor.app.type || "default") }); + } + if (actor.resource && !seen.has(actor.resource.name)) { + seen.add(actor.resource.name); + entities.push({ name: actor.resource.name, type: actor.resource.type, scope: actor.resource.scope, className: "bg-blue-50 text-blue-700", icon: iconFor(RESOURCE_ICONS, actor.resource.type) }); + } + } + } + return entities; +} + +function findingMetrics(f: AuditFinding): Record | undefined { + const m: Record = { ...f.evidence.metrics }; + if (f.context?.baseline?.deviationFactor) m["Deviation"] = `${f.context.baseline.deviationFactor}x`; + return Object.keys(m).length > 0 ? m : undefined; +} + +function formatShortDateTime(iso: string): string { + return new Date(iso).toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); +} + +function ActorPart({ icon, text, cls }: { icon?: React.ComponentType<{ className?: string }>; text: string; cls: string }) { + return {icon && React.createElement(icon, { className: "w-3 h-3" })}{text}; +} +function ActorCell({ actor }: { actor?: Actor }) { + if (!actor) return null; + const parts: React.ReactNode[] = []; + if (actor.identity) parts.push(); + if (actor.endpoint) parts.push(); + if (actor.app) parts.push(); + if (actor.resource) parts.push(); + return <>{parts.map((p, i) => {i > 0 && ·}{p})}; +} + +function EvidenceRows({ samples }: { samples: AuditSample[] }) { + const hasSrc = samples.some((s) => s.src); + const hasDst = samples.some((s) => s.dst); + const hasOk = samples.some((s) => s.succeeded != null); + const colCount = 2 + (hasSrc ? 1 : 0) + (hasDst ? 1 : 0) + (hasOk ? 1 : 0); + const th = "text-left pr-2 py-0.5 font-semibold"; + return ( + + + + + {hasSrc && } + + {hasDst && } + {hasOk && } + + + + {samples.map((s, i) => ( + + + + {hasSrc && } + + {hasDst && } + {hasOk && } + + {s.detail && ( + + + + )} + + ))} + +
TimeSourceActionDestinationOK
{formatShortDateTime(s.timestamp)}{s.action}{s.succeeded != null ? (s.succeeded ? "✓" : "✗") : ""}
{s.detail}
+ ); +} + +interface FindingProps { + id: string; title: string; summary: string; className?: string; + severity: { label: string; className?: string; dot?: string }; + subtitleTags?: BadgeProps[]; complianceTags?: BadgeProps[]; + timeRange?: { start: string; end: string; durationSeconds?: number }; + metrics?: Record; entities?: EntityEntry[]; + samples?: AuditSample[]; recommendation?: string; mitigations?: string[]; +} + +function Finding({ id, title, summary, severity, subtitleTags, complianceTags, className, + timeRange, metrics, entities, samples, recommendation, mitigations }: FindingProps) { + return ( +
+
+ {id} +

{title}

+ +
+ {subtitleTags && subtitleTags.length > 0 && ( +
+ {subtitleTags.map((t, i) => )} +
+ )} +

{summary}

+ {entities && entities.length > 0 && ( +
+ Affected Assets +
+ {entities.map((e, i) => ( + + ))} +
+
+ )} + {(samples?.length || timeRange || metrics) && ( +
+
+ Evidence + {timeRange && ( + + {formatDateTime(timeRange.start)} — {formatDateTime(timeRange.end)} + {timeRange.durationSeconds != null && ` (${formatDuration(timeRange.durationSeconds)})`} + + )} + {metrics && Object.entries(metrics).filter(([, v]) => v != null).map(([key, val]) => ( + {formatKey(key)} {typeof val === "number" ? val.toLocaleString() : String(val)} + ))} +
+ {samples && samples.length > 0 && } +
+ )} + {recommendation && ( +
+ Recommended Action +

{recommendation}

+ {mitigations && mitigations.length > 0 && ( +
    + {mitigations.map((m, i) =>
  1. {m}
  2. )} +
+ )} +
+ )} + {complianceTags && complianceTags.length > 0 && ( +
+ {complianceTags.map((t, i) => )} +
+ )} +
+ ); +} + + +function countBy(items: AuditFinding[], key: (f: AuditFinding) => string): { name: string; count: number }[] { + const map = new Map(); + for (const f of items) { const v = key(f); map.set(v, (map.get(v) || 0) + 1); } + return [...map.entries()].sort((a, b) => b[1] - a[1]).map(([name, count]) => ({ name, count })); +} + +function BreakdownTable({ title, rows, iconMap }: { + title: string; + rows: { name: string; count: number }[]; + iconMap?: (value: unknown) => React.ReactNode; +}) { + return ( + + ); +} + +function dedupDataSources(findings: AuditFinding[]): DataSource[] { + const seen = new Set(); + return findings.filter((f) => { + if (!f.dataSource) return false; + const key = f.dataSource.connection || f.dataSource.path || ""; + if (seen.has(key)) return false; + seen.add(key); + return true; + }).map((f) => f.dataSource!); +} + +function repoIcon(repo?: string): string { + if (!repo) return "git"; + if (repo.includes("github")) return "github"; + if (repo.includes("azure") || repo.includes("dev.azure")) return "azure-devops"; + if (repo.includes("gitlab")) return "gitlab"; + if (repo.includes("bitbucket")) return "bitbucket"; + return "git"; +} +function gitFileUrl(git: NonNullable): string | undefined { + if (!git.repo || !git.file) return undefined; + const sha = git.sha || git.branch || "main"; + if (git.repo.includes("github")) return `https://${git.repo}/blob/${sha}/${git.file}${git.lineNo ? `#L${git.lineNo}` : ""}`; + if (git.repo.includes("dev.azure")) return `https://${git.repo}?path=/${git.file}&version=GC${sha}${git.lineNo ? `&line=${git.lineNo}` : ""}`; + if (git.repo.includes("gitlab")) return `https://${git.repo}/-/blob/${sha}/${git.file}${git.lineNo ? `#L${git.lineNo}` : ""}`; + return undefined; +} +function fileTypeIcon(path?: string): string { + if (!path) return "vscode-icons:default-file"; + const ext = path.split(".").pop()?.toLowerCase(); + if (ext === "xlsx" || ext === "xls") return "vscode-icons:file-type-excel"; + if (ext === "csv") return "vscode-icons:file-type-excel2"; + if (ext === "json") return "vscode-icons:file-type-json"; + if (ext === "xml") return "vscode-icons:file-type-xml"; + if (ext === "parquet") return "vscode-icons:file-type-sql"; + if (ext === "yaml" || ext === "yml") return "vscode-icons:file-type-yaml"; + if (ext === "sql" || ext === "sqlaudit") return "vscode-icons:file-type-sql"; + if (ext === "log") return "vscode-icons:file-type-log"; + if (ext === "pdf") return "vscode-icons:file-type-pdf2"; + if (ext === "sqlite" || ext === "db") return "vscode-icons:file-type-sqlite"; + return "vscode-icons:default-file"; +} +function locationIcon(loc?: string): string { + if (!loc) return "server"; + if (loc === "sharepoint") return "sharepoint"; + if (loc === "google-drive") return "google-drive"; + if (loc === "onedrive") return "onedrive"; + if (loc === "network-share") return "server"; + return "server"; +} +const CATEGORY_BADGE: Record = { + "ai": "bg-purple-50 text-purple-600 border-purple-200", + "users": "bg-blue-50 text-blue-600 border-blue-200", + "groups": "bg-blue-50 text-blue-600 border-blue-200", + "roles": "bg-blue-50 text-blue-600 border-blue-200", + "access-logs": "bg-amber-50 text-amber-600 border-amber-200", + "audit-logs": "bg-amber-50 text-amber-600 border-amber-200", + "flow-logs": "bg-cyan-50 text-cyan-600 border-cyan-200", + "configuration": "bg-gray-100 text-gray-600 border-gray-200", +}; +function DataSourceCard({ ds }: { ds: DataSource }) { + const isFile = ds.type === "file" || ds.file; + const fileName = ds.file?.name || ds.path?.split("/").pop(); + const typeIcon = isFile ? fileTypeIcon(ds.file?.name || ds.path) : (ds.type || "database"); + const url = ds.git ? gitFileUrl(ds.git) : undefined; + const fullGitPath = ds.git ? [ds.git.repo, ds.git.file].filter(Boolean).join("/") + (ds.git.lineNo ? `:${ds.git.lineNo}` : "") : undefined; + const DsTypeIcon = typeIcon.startsWith("vscode-icons:") + ? + : ; + return ( +
+
+ {DsTypeIcon} + {isFile && fileName + ? {fileName} + : {ds.type || "file"}} + {ds.connection && {ds.connection}} + {ds.categories?.map((c) => ( + + ))} + + {ds.app && ( + + {ds.app.icon && } + {ds.app.name}{ds.app.version && ` v${ds.app.version}`} + + )} +
+ {ds.path &&
{ds.path}
} + {ds.file && ( +
+ {ds.file.location && ( + {ds.file.host || ds.file.location} + )} + {ds.file.size && {ds.file.size}} + {ds.file.created && Created {formatDateTime(ds.file.created)}} + {ds.file.modified && Modified {formatDateTime(ds.file.modified)}} +
+ )} + {ds.git && ( +
+ + {fullGitPath && (url + ? {fullGitPath} + : {fullGitPath} + )} + {ds.git.sha && {ds.git.sha}} + {ds.git.tag && {ds.git.tag}} + {ds.git.branch && {ds.git.branch}} +
+ )} + {ds.contentSha && ( +
+ + sha256:{ds.contentSha} +
+ )} + {ds.query &&
{ds.query}
} +
+ ); +} +function DataSourcesList({ findings }: { findings: AuditFinding[] }) { + const sources = dedupDataSources(findings); + const p = findings.find((f) => f.provenance)?.provenance; + const aiVendorIcon = (model?: string): string => { + if (!model) return "brain"; + const m = model.toLowerCase(); + if (m.includes("claude") || m.includes("anthropic")) return "claude"; + if (m.includes("gemini")) return "gemini"; + if (m.includes("gpt") || m.includes("openai") || m.includes("chatgpt")) return "openai"; + if (m.includes("ollama")) return "ollama"; + if (m.includes("mistral")) return "mistral"; + return "brain"; + }; + const aiCard: DataSource | null = p?.model ? { + type: aiVendorIcon(p.model), categories: ["ai"], + app: { name: p.generatedBy || "audit-log-analyzer", version: p.version, icon: aiVendorIcon(p.model) }, + connection: p.model, + path: p.runId ? `Run: ${p.runId}` : undefined, + } : null; + return ( +
+ {aiCard && } + {sources.map((ds, i) => )} +
+ ); +} + +function SummaryContent({ findings }: { findings: AuditFinding[] }) { + const criticalCount = findings.filter((f) => f.severity === "critical").length; + const highCount = findings.filter((f) => f.severity === "high").length; + const mediumCount = findings.filter((f) => f.severity === "medium").length; + + const outcomeCounts = countBy(findings, (f) => OUTCOME_STYLES[f.outcome].label); + const platformCounts = countBy(findings, (f) => PLATFORM_LABELS[f.platform]); + const categoryCounts = countBy(findings, (f) => f.category); + + return ( + <> +
+

Audit Findings Report

+

+ Generated {new Date().toLocaleDateString("en-ZA", { dateStyle: "long" })} · {findings.length} findings +

+
+ +
+ + + + +
+ +
+ { + const o = Object.entries(OUTCOME_STYLES).find(([, s]) => s.label === v); + return o ? : null; + }} /> + + { + const icon = CATEGORY_ICONS[v as string]; + return icon ? : null; + }} /> +
+ + + ); +} + +export default function FindingsReport(props: Record) { + const data = (props.data ?? props) as Record; + const findings: AuditFinding[] = Array.isArray(data.findings) ? data.findings + : Array.isArray(data) ? data : []; + const sorted = [...findings].sort((a, b) => SEVERITY_STYLES[a.severity].order - SEVERITY_STYLES[b.severity].order); + + const reportDate = new Date().toLocaleDateString("en-ZA", { dateStyle: "long" }); + return ( + +
+
+ + Audit Findings Report +
+
+
+
+ Confidential + {reportDate} + +
+
+ + + + + {sorted.map((f, i) => ( +
+ #{i + 1} + + {f.title} + {PLATFORM_LABELS[f.platform]} +
+ ))} +
+ + + + + {sorted.map((f, i) => ( + + ))} + +
+ ); +} diff --git a/report/catalog-report-types.ts b/report/catalog-report-types.ts index fc46af3f2..4b3560e11 100644 --- a/report/catalog-report-types.ts +++ b/report/catalog-report-types.ts @@ -82,6 +82,23 @@ export interface CatalogReportOptions { categoryMappings?: CatalogReportCategoryMapping[]; } +export interface CatalogReportGroupMember { + userId: string; + name: string; + email?: string; + userType?: string; + lastSignedInAt?: string; + membershipAddedAt: string; + membershipDeletedAt?: string; +} + +export interface CatalogReportGroup { + id: string; + name: string; + groupType?: string; + members: CatalogReportGroupMember[]; +} + export interface CatalogReportAudit { buildCommit: string; buildVersion: string; @@ -89,6 +106,7 @@ export interface CatalogReportAudit { options: CatalogReportOptions; scrapers: ScraperInfo[]; queries: QueryLogEntry[]; + groups: CatalogReportGroup[]; } export interface CatalogReportData { diff --git a/report/components/AuditPage.tsx b/report/components/AuditPage.tsx index 287d853ec..2d0da3e1f 100644 --- a/report/components/AuditPage.tsx +++ b/report/components/AuditPage.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, ListTable } from '@flanksource/facet'; +import { Badge, Section, ListTable } from '@flanksource/facet'; import type { CatalogReportAudit } from '../catalog-report-types.ts'; import ScraperCard from './ScraperCard.tsx'; @@ -19,9 +19,16 @@ function MetadataRow({ label, value }: { label: string; value?: string }) { function SectionBadge({ label, enabled }: { label: string; enabled: boolean }) { return ( - - {label} - + ); } @@ -118,6 +125,39 @@ export default function AuditPage({ audit }: Props) {
)} + + {audit.groups.length > 0 && ( +
+
+ {audit.groups.map((g) => ( +
+
+ {g.name} + {g.groupType && ( + ({g.groupType}) + )} + — {g.members.length} member(s) +
+ ({ + id: m.userId, + subject: m.email ? `${m.name} <${m.email}>` : m.name, + type: m.userType ?? '', + lastSignedIn: m.lastSignedInAt ?? '—', + added: m.membershipAddedAt, + removed: m.membershipDeletedAt ?? '', + }))} + subject="subject" + keys={['type', 'lastSignedIn', 'added', 'removed']} + size="xs" + density="compact" + cellClassName="text-xs font-mono" + /> +
+ ))} +
+
+ )} ); } diff --git a/report/components/CatalogAccessLogsSection.tsx b/report/components/CatalogAccessLogsSection.tsx index d7419371e..51c929121 100644 --- a/report/components/CatalogAccessLogsSection.tsx +++ b/report/components/CatalogAccessLogsSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, CompactTable } from '@flanksource/facet'; +import { Badge, Section, CompactTable } from '@flanksource/facet'; import type { CatalogReportAccessLog } from '../catalog-report-types.ts'; import { formatMonthDay, formatTime } from './utils.ts'; @@ -9,9 +9,9 @@ interface Props { function MFABadge({ mfa }: { mfa: boolean }) { if (mfa) { - return MFA; + return ; } - return no MFA; + return ; } export default function CatalogAccessLogsSection({ logs }: Props) { @@ -22,7 +22,7 @@ export default function CatalogAccessLogsSection({ logs }: Props) { log.createdAt ? `${formatMonthDay(log.createdAt)} ${formatTime(log.createdAt)}` : '-', , log.count > 1 ? ( - ×{log.count} + ) : ( '1' ), diff --git a/report/components/CatalogAccessSection.tsx b/report/components/CatalogAccessSection.tsx index c0d08e866..c465b1f5a 100644 --- a/report/components/CatalogAccessSection.tsx +++ b/report/components/CatalogAccessSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, CompactTable } from '@flanksource/facet'; +import { Badge, Section, CompactTable } from '@flanksource/facet'; import type { CatalogReportAccess } from '../catalog-report-types.ts'; import { formatMonthDay } from './utils.ts'; @@ -9,14 +9,14 @@ interface Props { function StaleBadge({ lastSignedInAt }: { lastSignedInAt?: string }) { if (!lastSignedInAt) { - return never; + return ; } const days = Math.floor((Date.now() - new Date(lastSignedInAt).getTime()) / 86400000); if (days > 90) { - return stale; + return ; } if (days > 30) { - return aging; + return ; } return null; } diff --git a/report/components/ConfigChangesSection.tsx b/report/components/ConfigChangesSection.tsx index 7739b7004..92010ac6c 100644 --- a/report/components/ConfigChangesSection.tsx +++ b/report/components/ConfigChangesSection.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Badge, Section, SeverityStatCard } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ConfigChange, ConfigSeverity } from '../config-types.ts'; -import { getChangeTypeLabel, getTypedChangeDisplay } from './change-section-utils.ts'; +import { getChangeTypeLabel, getTypedChangeDisplay, type TypedChangeDiff } from './change-section-utils.ts'; import { getTimeBucket, formatEntryDate, type TimeBucketFormat } from './utils.ts'; interface Props { @@ -76,7 +76,7 @@ function ChangeIcon({ change }: { change: ConfigChange }) { ); } -function ChangeTypeBadge({ change, label }: { change: ConfigChange; label: string }) { +function ChangeTypeBadge({ change, label, className }: { change: ConfigChange; label: string; className?: string }) { const accent = getChangeAccent(change, label); return ( @@ -88,7 +88,7 @@ function ChangeTypeBadge({ change, label }: { change: ConfigChange; label: strin color={accent.color} textColor={accent.textColor} borderColor={accent.borderColor} - className="shrink-0" + className={className ?? 'shrink-0'} /> ); } @@ -101,6 +101,46 @@ function SecondaryMeta({ label, className = 'text-gray-500' }: { label: string; ); } +function TypedDiffBadges({ diff }: { diff: TypedChangeDiff }) { + return ( + <> + {diff.label && ( + + )} + + + + + ); +} + function ChangeEntry({ change, dateFormat, hideConfigName }: { change: ConfigChange; dateFormat: TimeBucketFormat; hideConfigName?: boolean }) { const sev = change.severity ?? 'info'; const author = change.createdBy || change.externalCreatedBy || change.source || ''; @@ -109,13 +149,8 @@ function ChangeEntry({ change, dateFormat, hideConfigName }: { change: ConfigCha const summary = change.summary || typedDisplay?.summary; const changeTypeLabel = getChangeTypeLabel(change, typedDisplay); const hasSecondaryMeta = sev !== 'info' || Boolean(author); - const hasPrimaryMeta = Boolean( - typedDisplay?.diff - || (typedDisplay?.meta && typedDisplay.meta.length > 0) - || (!hideConfigName && change.configName) - || (change.count ?? 0) > 1 - || artifactCount > 0, - ); + const inlineMetaBadgeClass = 'align-middle mb-[0.35mm] max-w-full whitespace-normal break-all'; + const inlineTypeBadgeClass = 'align-middle mr-[0.8mm] mb-[0.35mm]'; return (
@@ -123,85 +158,76 @@ function ChangeEntry({ change, dateFormat, hideConfigName }: { change: ConfigCha
-
-
- - {summary && ( -
- {summary} -
- )} -
- {hasPrimaryMeta && ( -
- {typedDisplay?.diff && ( - - {typedDisplay.diff.label && ( - - {typedDisplay.diff.label} - - )} - - {typedDisplay.diff.from} - - - - {typedDisplay.diff.to} - - - )} - {!hideConfigName && change.configName && ( - - )} - {typedDisplay?.meta?.map((meta) => ( - - ))} - {(change.count ?? 0) > 1 && ( +
+ + {summary && {summary}} + {typedDisplay?.diff && ( + <> + {' '} + + + )} + {!hideConfigName && change.configName && ( + <> + {' '} + + + )} + {typedDisplay?.meta?.map((meta) => ( + + {' '} + + + ))} + {(change.count ?? 0) > 1 && ( + <> + {' '} + + + )} + {artifactCount > 0 && ( + <> + {' '} + 1 ? 's' : ''}`} + color="bg-purple-50" + textColor="text-purple-700" + borderColor="border-purple-200" + className={inlineMetaBadgeClass} /> - )} - {artifactCount > 0 && ( - - 1 ? 's' : ''}`} - color="bg-purple-50" - textColor="text-purple-700" - borderColor="border-purple-200" - className="shrink-0" - /> - - )} -
+ + )}
{hasSecondaryMeta && ( diff --git a/report/components/ConfigInsightsSection.tsx b/report/components/ConfigInsightsSection.tsx index 9cf80900a..fa53ff103 100644 --- a/report/components/ConfigInsightsSection.tsx +++ b/report/components/ConfigInsightsSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, SeverityStatCard } from '@flanksource/facet'; +import { Badge, Section, SeverityStatCard } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ConfigAnalysis, ConfigSeverity, AnalysisType } from '../config-types.ts'; import { formatDate } from './utils.ts'; @@ -44,16 +44,12 @@ function InsightEntry({ analysis }: { analysis: ConfigAnalysis }) { {analysis.analyzer} {analysis.configName && ( - {analysis.configName} + )} {analysis.message || analysis.summary || '-'} - - {sev} - + {analysis.status && ( - - {analysis.status} - + )} {analysis.lastObserved && ( {formatDate(analysis.lastObserved)} @@ -76,9 +72,7 @@ function AnalysisTypeGroup({ type, analyses }: { type: string; analyses: ConfigA
{type} - - {analyses.length} - +
{sorted.map((a) => )} diff --git a/report/components/ConfigItemCard.tsx b/report/components/ConfigItemCard.tsx index 4c2cb9227..a882b2e29 100644 --- a/report/components/ConfigItemCard.tsx +++ b/report/components/ConfigItemCard.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Badge } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ConfigItem } from '../rbac-types.ts'; import { formatDate } from './utils.ts'; @@ -16,10 +17,17 @@ export default function ConfigItemCard({ config }: Props) { {config.type && } {config.name} {Object.entries(tags).map(([k, v]) => ( - - {k} - {v || '-'} - + ))}
diff --git a/report/components/CoverPage.tsx b/report/components/CoverPage.tsx index aa0fab229..b562ecbd0 100644 --- a/report/components/CoverPage.tsx +++ b/report/components/CoverPage.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Badge } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import { formatDate, formatDateTime } from './utils.ts'; @@ -33,9 +34,7 @@ function SubjectBadge({ subject }: { subject: CoverPageSubject }) { {subject.name} {subject.type} {subject.status && ( - - {subject.status} - + )}
); @@ -46,10 +45,17 @@ function TagBadges({ tags }: { tags: Record }) { return (
{Object.entries(tags).map(([k, v]) => ( - - {k} - {v || '-'} - + ))}
); diff --git a/report/components/DynamicSection.tsx b/report/components/DynamicSection.tsx index 4b8931e87..348013596 100644 --- a/report/components/DynamicSection.tsx +++ b/report/components/DynamicSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, CompactTable } from '@flanksource/facet'; +import { Badge, Section, CompactTable } from '@flanksource/facet'; import type { ApplicationSection, ViewColumnType } from '../types.ts'; import RBACChanges from './RBACChanges.tsx'; import BackupChanges from './BackupChanges.tsx'; @@ -31,19 +31,26 @@ const HEALTH_CLASSES: Record = { unknown: 'bg-gray-400', }; -const REFRESH_CLASSES: Record = { - fresh: 'bg-green-100 text-green-800', - cache: 'bg-yellow-100 text-yellow-800', +const REFRESH_STYLES: Record = { + fresh: { color: 'bg-green-100', textColor: 'text-green-800', borderColor: 'border-green-200' }, + cache: { color: 'bg-yellow-100', textColor: 'text-yellow-800', borderColor: 'border-yellow-200' }, }; function TagBadges({ value }: { value: Record }) { return ( {Object.entries(value).map(([k, v]) => ( - - {k} - {v} - + ))} ); @@ -92,9 +99,7 @@ function SeverityBadge({ severity }: { severity: string }) { const color = SEVERITY_COLORS[key] ?? '#6B7280'; const bg = SEVERITY_BG[key] ?? '#F3F4F6'; return ( - - {severity} - + ); } @@ -105,8 +110,8 @@ function ViewSection({ section }: { section: ApplicationSection }) { const visibleCols = view.columns.filter((c) => !c.hidden); const headers = visibleCols.map((c) => c.name.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())); - const refreshClass = view.refreshStatus - ? (REFRESH_CLASSES[view.refreshStatus] ?? 'bg-red-100 text-red-800') + const refreshStyle = view.refreshStatus + ? (REFRESH_STYLES[view.refreshStatus] ?? { color: 'bg-red-100', textColor: 'text-red-800', borderColor: 'border-red-200' }) : null; const rows = view.rows.map((row) => { @@ -123,9 +128,16 @@ function ViewSection({ section }: { section: ApplicationSection }) {
{view.refreshStatus && ( - - {view.refreshStatus} - + )} {view.lastRefreshedAt && ( diff --git a/report/components/GitRef.tsx b/report/components/GitRef.tsx index c057c9dd1..76c33c284 100644 --- a/report/components/GitRef.tsx +++ b/report/components/GitRef.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Badge } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; interface GitRefProps { @@ -17,9 +18,16 @@ const SIZE_CLASSES = { function Tag({ children, className = '' }: { children: React.ReactNode; className?: string }) { return ( - - {children} - + ); } diff --git a/report/components/RBACChangelogSection.tsx b/report/components/RBACChangelogSection.tsx index 604c42c03..6664d6c5d 100644 --- a/report/components/RBACChangelogSection.tsx +++ b/report/components/RBACChangelogSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section } from '@flanksource/facet'; +import { Badge, Section } from '@flanksource/facet'; import type { RBACChangeEntry } from '../rbac-types.ts'; import { formatDate } from './utils.ts'; @@ -12,12 +12,16 @@ const CHANGELOG_TYPE_COLORS: Record = { function ChangeTypeBadge({ type }: { type: string }) { const colors = CHANGELOG_TYPE_COLORS[type] || { bg: '#E2E8F0', fg: '#334155' }; return ( - - {type} - + ); } diff --git a/report/components/RBACMatrixSection.tsx b/report/components/RBACMatrixSection.tsx index f6f49fa81..8b88ab4f9 100644 --- a/report/components/RBACMatrixSection.tsx +++ b/report/components/RBACMatrixSection.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Icon } from '@flanksource/icons/icon'; import type { RBACResource, RBACUserRole } from '../rbac-types.ts'; -import { MatrixTable, Dot } from '@flanksource/facet'; +import { Badge, MatrixTable, Dot } from '@flanksource/facet'; import { ACCESS_COLORS, STALE_COLORS, ReviewOverdueBadge, ReviewOverdueLegendSwatch, IdentityIcon } from './rbac-visual.tsx'; interface Props { @@ -132,10 +132,17 @@ export default function RBACMatrixSection({ resource }: Props) { {Object.keys(tags).length > 0 && (
{Object.entries(tags).map(([k, v]) => ( - - {k} - {v || '-'} - + ))}
)} diff --git a/report/components/RBACUserSection.tsx b/report/components/RBACUserSection.tsx index 2c04c8a66..56e0ea80b 100644 --- a/report/components/RBACUserSection.tsx +++ b/report/components/RBACUserSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { CompactTable } from '@flanksource/facet'; +import { Badge, CompactTable } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { RBACUserReport, RBACUserResource } from '../rbac-types.ts'; import { ConfigTypeIcon } from './configTypeIcon.tsx'; @@ -38,12 +38,16 @@ function RoleSourceBadge({ source }: { source: string }) { const key = source.startsWith('group:') ? 'group' : source; const colors = ROLE_SOURCE_COLORS[key] || ROLE_SOURCE_COLORS.direct; return ( - - {source} - + ); } diff --git a/report/components/ScraperCard.tsx b/report/components/ScraperCard.tsx index f70fabaea..8f0665e5b 100644 --- a/report/components/ScraperCard.tsx +++ b/report/components/ScraperCard.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Badge } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ScraperInfo } from '../scraper-types.ts'; import { formatDate } from './utils.ts'; @@ -21,9 +22,16 @@ const TYPE_ICONS: Record = { function Tag({ children }: { children: React.ReactNode }) { return ( - - {children} - + ); } @@ -39,9 +47,16 @@ export default function ScraperCard({ scraper }: Props) { ))} {scraper.name} {scraper.source && ( - - {scraper.source} - + )} {scraper.id.slice(0, 8)} {scraper.createdBy && {scraper.createdBy}} diff --git a/report/components/ViewResultSection.tsx b/report/components/ViewResultSection.tsx index 54902c703..e8bee0603 100644 --- a/report/components/ViewResultSection.tsx +++ b/report/components/ViewResultSection.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Section, CompactTable } from '@flanksource/facet'; +import { Badge, Section, CompactTable } from '@flanksource/facet'; import { Icon } from '@flanksource/icons/icon'; import type { ViewReportData, ViewColumnDef, @@ -50,12 +50,7 @@ const STATUS_COLORS: Record = { function StatusBadge({ value }: { value: string }) { const colors = STATUS_COLORS[value.toLowerCase()] ?? { bg: '#F3F4F6', fg: '#374151' }; return ( - - {value} - + ); } @@ -72,12 +67,7 @@ function BadgeCell({ value, config }: { value: string; config?: BadgeConfig }) { } return ( - - {value} - + ); } @@ -131,10 +121,17 @@ function TagBadges({ value }: { value: Record }) { return ( {Object.entries(value).map(([k, v]) => ( - - {k} - {v} - + ))} ); @@ -1021,10 +1018,17 @@ export default function ViewResultSection({ data }: Props) { {data.variables && data.variables.length > 0 && (
{data.variables.map((v) => ( - - {v.label || v.key}: - {v.default || '-'} - + ))}
)} diff --git a/report/icons.ts b/report/icons.ts new file mode 100644 index 000000000..7bdd70100 --- /dev/null +++ b/report/icons.ts @@ -0,0 +1,219 @@ +// Audit Findings Icon Set — Selected from REQUIREMENTS-audit-icon-set.pdf +// Sources: Phosphor (ph/256), MDI (24), Tabler (24), Lucide (24), Icons8 (50) +// Render: + +export interface IconDef { body: string; viewBox: string } + +function ph(body: string): IconDef { return { body, viewBox: "0 0 256 256" }; } +function mdi(body: string): IconDef { return { body, viewBox: "0 0 24 24" }; } +function tabler(body: string): IconDef { return { body, viewBox: "0 0 24 24" }; } +function lucide(body: string): IconDef { return { body, viewBox: "0 0 24 24" }; } + +// ── Outcomes ──────────────────────────────────────────────────────── + +// ph:power +export const ICON_SAFETY_SWITCH = ph(``); + +// icons8:fire-alarm (kept as tabler:bell-ringing equivalent — stroke 24x24) +export const ICON_PAGE_ONCALL = tabler(``); + +// icons8:high-risk (kept as tabler:alert-triangle — stroke 24x24) +export const ICON_HIGH_TICKET = tabler(``); + +// icons8:ticket (kept as tabler:ticket — stroke 24x24) +export const ICON_LOW_TICKET = tabler(``); + +// ph:scroll +export const ICON_INFORMATIONAL = ph(``); + +// ── Attack Categories ─────────────────────────────────────────────── + +// mdi:key-alert +export const ICON_CREDENTIAL_ATTACK = mdi(``); + +// mdi:shield-account +export const ICON_PRIVILEGE_ESCALATION = mdi(``); + +// tabler:stack-push +export const ICON_PRIVILEGE_ACCUMULATION = tabler(``); + +// mdi:database-export +export const ICON_DATA_EXFILTRATION = mdi(``); + +// (no selection — kept original) +export const ICON_LATERAL_MOVEMENT = tabler(``); + +// (no selection — kept original) +export const ICON_PERSISTENCE = tabler(``); + +// lucide:shredder +export const ICON_AUDIT_TAMPERING = lucide(``); + +// ph:moon-stars +export const ICON_AFTER_HOURS = ph(``); + +// icons8:hammer (kept as tabler:hammer — stroke 24x24) +export const ICON_BREAK_GLASS = tabler(``); + +// mdi:account-switch +export const ICON_SHARED_ACCOUNT = mdi(``); + +// (no selection — kept original) +export const ICON_SERVICE_ACCOUNT_MISUSE = tabler(``); + +// mdi:firewall (alias: wall-fire) +export const ICON_NETWORK_EXPOSURE = mdi(``); + +// ph:bomb +export const ICON_DESTRUCTIVE_ACTION = ph(``); + +// ph:eye-slash +export const ICON_COVERAGE_GAP = ph(``); + +// ── Kill Chain Phases (unique, others reuse above) ────────────────── + +// mdi:radar +export const ICON_RECONNAISSANCE = mdi(``); + +// icons8:door-opened (kept as tabler equivalent — stroke 24x24) +export const ICON_INITIAL_ACCESS = tabler(``); + +// mdi:database-search +export const ICON_COLLECTION = mdi(``); + +// mdi:database-export +export const ICON_EXFILTRATION = mdi(``); + +// mdi:flash-alert +export const ICON_IMPACT = mdi(``); + +// ── Actor Type Icons ───────────────────────────────────────────────�� + +// tabler:user (human identity) +export const ICON_IDENTITY_HUMAN = tabler(``); + +// tabler:robot (machine identity) +export const ICON_IDENTITY_MACHINE = tabler(``); + +// tabler:crown (root identity) +export const ICON_IDENTITY_ROOT = tabler(``); + +// tabler:question-mark (unknown identity) +export const ICON_IDENTITY_UNKNOWN = tabler(``); + +// tabler:world (IP / endpoint) +export const ICON_ENDPOINT_IP = tabler(``); + +// tabler:device-desktop (workstation endpoint) +export const ICON_ENDPOINT_WORKSTATION = tabler(``); + +// tabler:server (server endpoint) +export const ICON_ENDPOINT_SERVER = tabler(``); + +// tabler:app-window (app reference) +export const ICON_APP = tabler(``); + +// mdi:database +export const ICON_DATABASE = mdi(``); + +// mdi:lock +export const ICON_SECRET = mdi(``); + +// ── Provenance & Data Source Icons ───────────────────────────────── + +// tabler:brain (AI model) +export const ICON_AI_MODEL = tabler(``); + +// tabler:clock (timestamp) +export const ICON_CLOCK = tabler(``); + +// tabler:hash (run ID) +export const ICON_HASH = tabler(``); + +// tabler:tool (analyzer tool) +export const ICON_TOOL = tabler(``); + +// tabler:tag (version) +export const ICON_VERSION = tabler(``); + +// tabler:bucket (S3/storage) +export const ICON_BUCKET = tabler(``); + +// tabler:file-analytics (audit log) +export const ICON_AUDIT_LOG = tabler(``); + +// tabler:cloud (cloud trail / cloud source) +export const ICON_CLOUD = tabler(``); + +// ── Lookup Maps ───────────────────────────────────────────────────── + +export const OUTCOME_ICONS: Record = { + "safety-switch": ICON_SAFETY_SWITCH, + "page-oncall": ICON_PAGE_ONCALL, + "high-ticket": ICON_HIGH_TICKET, + "low-ticket": ICON_LOW_TICKET, + "informational": ICON_INFORMATIONAL, +}; + +export const CATEGORY_ICONS: Record = { + "credential-attack": ICON_CREDENTIAL_ATTACK, + "privilege-escalation": ICON_PRIVILEGE_ESCALATION, + "privilege-accumulation": ICON_PRIVILEGE_ACCUMULATION, + "data-exfiltration": ICON_DATA_EXFILTRATION, + "lateral-movement": ICON_LATERAL_MOVEMENT, + "persistence": ICON_PERSISTENCE, + "audit-tampering": ICON_AUDIT_TAMPERING, + "after-hours": ICON_AFTER_HOURS, + "break-glass": ICON_BREAK_GLASS, + "shared-account": ICON_SHARED_ACCOUNT, + "service-account-misuse": ICON_SERVICE_ACCOUNT_MISUSE, + "network-exposure": ICON_NETWORK_EXPOSURE, + "destructive-action": ICON_DESTRUCTIVE_ACTION, + "coverage-gap": ICON_COVERAGE_GAP, +}; + +export const KILL_CHAIN_ICONS: Record = { + "reconnaissance": ICON_RECONNAISSANCE, + "initial-access": ICON_INITIAL_ACCESS, + "persistence": ICON_PERSISTENCE, + "privilege-escalation": ICON_PRIVILEGE_ESCALATION, + "lateral-movement": ICON_LATERAL_MOVEMENT, + "collection": ICON_COLLECTION, + "exfiltration": ICON_EXFILTRATION, + "impact": ICON_IMPACT, +}; + +export const IDENTITY_ICONS: Record = { + "human": ICON_IDENTITY_HUMAN, + "service-account": ICON_SERVICE_ACCOUNT_MISUSE, + "break-glass": ICON_BREAK_GLASS, + "admin": ICON_PRIVILEGE_ESCALATION, + "machine": ICON_IDENTITY_MACHINE, + "root": ICON_IDENTITY_ROOT, + "unknown": ICON_IDENTITY_UNKNOWN, +}; + +export const ENDPOINT_ICONS: Record = { + "ip": ICON_ENDPOINT_IP, + "workstation": ICON_ENDPOINT_WORKSTATION, + "server": ICON_ENDPOINT_SERVER, + "vpn": ICON_ENDPOINT_IP, +}; + +export const RESOURCE_ICONS: Record = { + "database": ICON_DATABASE, + "role": ICON_PRIVILEGE_ESCALATION, + "clusterrolebinding": ICON_PRIVILEGE_ESCALATION, + "clusterrole": ICON_PRIVILEGE_ESCALATION, + "security-group": ICON_NETWORK_EXPOSURE, + "rds-instance": ICON_DATABASE, + "audit-type": ICON_AUDIT_TAMPERING, + "namespace": ICON_LATERAL_MOVEMENT, + "secret": ICON_SECRET, + "s3-bucket": ICON_DATA_EXFILTRATION, +}; + +export const APP_ICONS: Record = { + "default": ICON_APP, +}; + diff --git a/report/kitchen-sink/CatalogPage.tsx b/report/kitchen-sink/CatalogPage.tsx index 3dfa797ef..307ba6c21 100644 --- a/report/kitchen-sink/CatalogPage.tsx +++ b/report/kitchen-sink/CatalogPage.tsx @@ -6,6 +6,7 @@ import CatalogAccessSection from '../components/CatalogAccessSection.tsx'; import CatalogAccessLogsSection from '../components/CatalogAccessLogsSection.tsx'; import CatalogList from '../components/CatalogList.tsx'; import ArtifactAppendix from '../components/ArtifactAppendix.tsx'; +import AuditPage from '../components/AuditPage.tsx'; interface Props { data: KitchenSinkData; @@ -20,7 +21,7 @@ export default function CatalogPage({ data, pageProps }: Props) {
- Components used in the Catalog report: config tree, access control, access logs, catalog list, and artifact appendix. + Components used in the Catalog report: config tree, access control, access logs, catalog list, artifact appendix, and the --audit page (build metadata, queries, scrapers, and group membership).
@@ -32,6 +33,15 @@ export default function CatalogPage({ data, pageProps }: Props) { + + {catalog.audit && ( +
+
+ The --audit page: build/options metadata, queries, scrapers, and group membership for every external group referenced by config_access on the reported configs. +
+ +
+ )}
); } diff --git a/report/package.json b/report/package.json index 3a305d2c2..8122da3af 100644 --- a/report/package.json +++ b/report/package.json @@ -11,6 +11,15 @@ "dependencies": { "@flanksource/facet": "file:/Users/moshe/go/src/github.com/flanksource/facet", "@flanksource/icons": "^1.0.53", + "@iconify-json/carbon": "^1.2.0", + "@iconify-json/fluent": "^1.2.0", + "@iconify-json/iconoir": "^1.2.0", "@iconify-json/lucide": "^1.2.0", + "@iconify-json/mdi": "^1.2.0", + "@iconify-json/ph": "^1.2.0", + "@iconify-json/ri": "^1.2.0", + "@iconify-json/tabler": "^1.2.0", + "@iconify-json/vscode-icons": "^1.2.45", + "@iconify/react": "^5.1.0", "js-yaml": "^4.1.0" }, "devDependencies": { diff --git a/report/testdata/kitchen-sink.yaml b/report/testdata/kitchen-sink.yaml index 3143ff31e..59724c570 100644 --- a/report/testdata/kitchen-sink.yaml +++ b/report/testdata/kitchen-sink.yaml @@ -1661,6 +1661,80 @@ catalogReport: filename: "policy-diff.yaml" contentType: "text/yaml" size: 1200 + audit: + buildCommit: "3b3a1a0f" + buildVersion: "v1.47.0" + gitStatus: " M catalog_report/report.go\n M db/rbac.go" + options: + title: "Production EKS Cluster Report" + since: "720h" + sections: + changes: true + insights: true + relationships: true + access: true + accessLogs: true + configJSON: false + recursive: true + groupBy: "type" + changeArtifacts: true + thresholds: + staleDays: 90 + reviewOverdueDays: 180 + filters: + - "type=AWS::EKS::Cluster" + - "namespace=mc" + scrapers: [] + queries: + - name: "RBACAccess" + args: "configIDs=1 selectors=0" + count: 42 + duration: 128 + pretty: "SELECT ... FROM config_access_summary WHERE config_id IN (?)" + - name: "GroupMembers" + args: "configIDs=1" + count: 5 + duration: 34 + pretty: "SELECT ... FROM external_user_groups eug JOIN external_groups eg ..." + groups: + - id: "grp-admins" + name: "mission-control-admins" + groupType: "group" + members: + - userId: "u-alice" + name: "Alice Johnson" + email: "alice@flanksource.com" + userType: "user" + lastSignedInAt: "2026-03-30T08:00:00Z" + membershipAddedAt: "2025-01-10T09:00:00Z" + - userId: "u-bob" + name: "Bob Smith" + email: "bob@flanksource.com" + userType: "user" + lastSignedInAt: "2026-03-28T14:00:00Z" + membershipAddedAt: "2025-06-01T09:00:00Z" + - userId: "u-stale" + name: "Former Employee" + email: "former@flanksource.com" + userType: "user" + lastSignedInAt: "2025-06-15T10:00:00Z" + membershipAddedAt: "2024-06-01T09:00:00Z" + membershipDeletedAt: "2025-07-01T09:00:00Z" + - id: "grp-readers" + name: "mission-control-readers" + groupType: "group" + members: + - userId: "u-carol" + name: "Carol Davis" + email: "carol@flanksource.com" + userType: "user" + membershipAddedAt: "2026-01-10T09:00:00Z" + - userId: "u-deploy-bot" + name: "deploy-bot" + email: "deploy-bot@flanksource.com" + userType: "service_account" + lastSignedInAt: "2026-03-30T06:00:00Z" + membershipAddedAt: "2025-08-01T09:00:00Z" viewReport: name: "cluster-overview" From 7e5685652ea4aa9aa004d49845348f31de210170 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 10 Apr 2026 14:26:16 +0300 Subject: [PATCH 35/48] refactor(catalog)!: move catalog_report package to report/catalog with enhanced change mappings Reorganizes catalog_report package into report/catalog namespace and redesigns category mappings to use CEL-based filters and transforms instead of simple string lists. Adds scraper report extraction, group membership auditing, change details deserialization, limit/pagination controls, and cycle protection for config tree traversal. BREAKING CHANGE: move catalog_report package to report/catalog with enhanced change mappings --- api/catalog_report.go | 58 +++- api/scraper_report.go | 17 + application/render_facet.go | 12 +- catalog_report/default-settings.yaml | 42 --- catalog_report/report_test.go | 69 ---- cmd/catalog_get_test.go | 6 +- cmd/catalog_report.go | 31 +- cmd/view.go | 2 +- db/rbac_test.go | 68 ++++ rbac_report/render_facet.go | 4 +- report/catalog/change_mappings.go | 308 ++++++++++++++++++ report/catalog/change_mappings_test.go | 100 ++++++ report/catalog/default-settings.yaml | 34 ++ {catalog_report => report/catalog}/export.go | 76 ++++- .../catalog}/render_facet.go | 2 +- {catalog_report => report/catalog}/report.go | 230 ++++++++++--- report/catalog/report_test.go | 185 +++++++++++ .../catalog}/settings.go | 51 ++- .../catalog}/settings_test.go | 59 ++-- report/scraper/scraper.go | 104 ++++++ report/testdata/kitchen-sink.yaml | 2 +- views/render_facet.go | 6 +- 22 files changed, 1231 insertions(+), 235 deletions(-) create mode 100644 api/scraper_report.go delete mode 100644 catalog_report/default-settings.yaml delete mode 100644 catalog_report/report_test.go create mode 100644 db/rbac_test.go create mode 100644 report/catalog/change_mappings.go create mode 100644 report/catalog/change_mappings_test.go create mode 100644 report/catalog/default-settings.yaml rename {catalog_report => report/catalog}/export.go (69%) rename {catalog_report => report/catalog}/render_facet.go (97%) rename {catalog_report => report/catalog}/report.go (71%) create mode 100644 report/catalog/report_test.go rename {catalog_report => report/catalog}/settings.go (58%) rename {catalog_report => report/catalog}/settings_test.go (66%) create mode 100644 report/scraper/scraper.go diff --git a/api/catalog_report.go b/api/catalog_report.go index 9666456e5..dbc1b5cef 100644 --- a/api/catalog_report.go +++ b/api/catalog_report.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "time" @@ -19,16 +20,22 @@ type CatalogReportThresholds struct { ReviewOverdueDays int `json:"reviewOverdueDays"` } +type CatalogReportCategoryMapping struct { + Category string `json:"category,omitempty"` + Filter string `json:"filter"` + Transform string `json:"transform,omitempty"` +} + type CatalogReportOptions struct { - Title string `json:"title"` - Since string `json:"since"` - Sections CatalogReportSections `json:"sections"` - Recursive bool `json:"recursive"` - GroupBy string `json:"groupBy"` - ChangeArtifacts bool `json:"changeArtifacts"` - Filters []string `json:"filters,omitempty"` - Thresholds *CatalogReportThresholds `json:"thresholds,omitempty"` - CategoryMappings map[string][]string `json:"categoryMappings,omitempty"` + Title string `json:"title"` + Since string `json:"since"` + Sections CatalogReportSections `json:"sections"` + Recursive bool `json:"recursive"` + GroupBy string `json:"groupBy"` + ChangeArtifacts bool `json:"changeArtifacts"` + Filters []string `json:"filters,omitempty"` + Thresholds *CatalogReportThresholds `json:"thresholds,omitempty"` + CategoryMappings []CatalogReportCategoryMapping `json:"categoryMappings,omitempty"` } type CatalogReportAudit struct { @@ -38,6 +45,24 @@ type CatalogReportAudit struct { Options CatalogReportOptions `json:"options"` Scrapers []ScraperInfo `json:"scrapers"` Queries []CatalogReportQuery `json:"queries"` + Groups []CatalogReportGroup `json:"groups"` +} + +type CatalogReportGroup struct { + ID string `json:"id"` + Name string `json:"name"` + GroupType string `json:"groupType,omitempty"` + Members []CatalogReportGroupMember `json:"members"` +} + +type CatalogReportGroupMember struct { + UserID string `json:"userId"` + Name string `json:"name"` + Email string `json:"email,omitempty"` + UserType string `json:"userType,omitempty"` + LastSignedInAt *string `json:"lastSignedInAt,omitempty"` + MembershipAddedAt string `json:"membershipAddedAt"` + MembershipDeletedAt *string `json:"membershipDeletedAt,omitempty"` } type CatalogReportQuery struct { @@ -47,6 +72,7 @@ type CatalogReportQuery struct { Duration int64 `json:"duration"` Error string `json:"error,omitempty"` Summary string `json:"summary,omitempty"` + Pretty string `json:"pretty"` } type CatalogReport struct { @@ -60,9 +86,9 @@ type CatalogReport struct { GroupBy string `json:"groupBy,omitempty"` Entries []CatalogReportEntry `json:"entries"` - CategoryMappings map[string][]string `json:"categoryMappings,omitempty"` - Thresholds *CatalogReportThresholds `json:"thresholds,omitempty"` - Audit *CatalogReportAudit `json:"audit,omitempty"` + CategoryMappings []CatalogReportCategoryMapping `json:"categoryMappings,omitempty"` + Thresholds *CatalogReportThresholds `json:"thresholds,omitempty"` + Audit *CatalogReportAudit `json:"audit,omitempty"` // Deprecated: use Entries[0] for single-config reports ConfigItem models.ConfigItem `json:"configItem"` @@ -127,10 +153,12 @@ type CatalogReportChange struct { ConfigType string `json:"configType,omitempty"` Permalink string `json:"permalink,omitempty"` ChangeType string `json:"changeType"` + Category string `json:"category,omitempty"` Severity string `json:"severity,omitempty"` Source string `json:"source,omitempty"` Summary string `json:"summary,omitempty"` Details map[string]any `json:"details,omitempty"` + TypedChange map[string]any `json:"typedChange,omitempty"` CreatedBy string `json:"createdBy,omitempty"` ExternalCreatedBy string `json:"externalCreatedBy,omitempty"` CreatedAt string `json:"createdAt,omitempty"` @@ -158,6 +186,12 @@ func NewCatalogReportChange(c models.ConfigChange, configName, configType string if c.ExternalCreatedBy != nil { r.ExternalCreatedBy = *c.ExternalCreatedBy } + if len(c.Details) > 0 { + var details map[string]any + if err := json.Unmarshal(c.Details, &details); err == nil { + r.Details = details + } + } return r } diff --git a/api/scraper_report.go b/api/scraper_report.go new file mode 100644 index 000000000..829da3d3d --- /dev/null +++ b/api/scraper_report.go @@ -0,0 +1,17 @@ +package api + +import "github.com/flanksource/duty/query" + +type ScraperInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` + Description string `json:"description,omitempty"` + Source string `json:"source,omitempty"` + Types []string `json:"types"` + SpecHash string `json:"specHash"` + CreatedBy string `json:"createdBy,omitempty"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt,omitempty"` + GitOps *query.GitOpsSource `json:"gitops,omitempty"` +} diff --git a/application/render_facet.go b/application/render_facet.go index 3ba188fe4..9df0553a8 100644 --- a/application/render_facet.go +++ b/application/render_facet.go @@ -11,14 +11,22 @@ func RenderFacetHTML(app *icapi.Application) ([]byte, error) { if app == nil { return nil, fmt.Errorf("application must not be nil") } - return report.RenderCLI(initSlices(app), "html", "Application.tsx") + result, err := report.RenderCLI(initSlices(app), "html", "Application.tsx") + if err != nil { + return nil, err + } + return result.Data, nil } func RenderFacetPDF(app *icapi.Application) ([]byte, error) { if app == nil { return nil, fmt.Errorf("application must not be nil") } - return report.RenderCLI(initSlices(app), "pdf", "Application.tsx") + result, err := report.RenderCLI(initSlices(app), "pdf", "Application.tsx") + if err != nil { + return nil, err + } + return result.Data, nil } func initSlices(app *icapi.Application) icapi.Application { diff --git a/catalog_report/default-settings.yaml b/catalog_report/default-settings.yaml deleted file mode 100644 index 1f2e3c6c8..000000000 --- a/catalog_report/default-settings.yaml +++ /dev/null @@ -1,42 +0,0 @@ -filters: - - "type!=Kubernetes::ConfigMap" - - "type!=Kubernetes::Secret" - - "type!=Kubernetes::Event" - -thresholds: - staleDays: 90 - reviewOverdueDays: 90 - -categoryMappings: - rbac.granted: - - PermissionGranted - - PermissionAdded - - IAMRoleAdded - rbac.revoked: - - PermissionRevoked - - PermissionRemoved - - IAMRoleRemoved - backup.success: - - BackupCompleted - - BackupSuccessful - - BACKUP_DB@low - backup.failed: - - BackupFailed - - BACKUP_DB@high - backup.progress: - - BackupStarted - - BackupRunning - - BackupEnqueued - backup.restore: - - BackupRestored - - RestoreCompleted - deployment.spec: - - diff - deployment.success: - - CodeDeployment@info - deployment.failed: - - CodeDeployment@failed - deployment.scale: - - ScalingReplicaSet - deployment.policy: - - PolicyUpdate diff --git a/catalog_report/report_test.go b/catalog_report/report_test.go deleted file mode 100644 index cfbaf205f..000000000 --- a/catalog_report/report_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package catalog_report - -import ( - "testing" - "time" - - ginkgo "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/flanksource/incident-commander/api" -) - -func TestCatalogReport(t *testing.T) { - RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, "CatalogReport") -} - -var _ = ginkgo.Describe("Options", func() { - ginkgo.It("WithDefaults sets 30-day since", func() { - opts := Options{}.WithDefaults() - Expect(opts.Since).To(Equal(30 * 24 * time.Hour)) - }) - - ginkgo.It("WithDefaults preserves custom since", func() { - opts := Options{Since: 7 * 24 * time.Hour}.WithDefaults() - Expect(opts.Since).To(Equal(7 * 24 * time.Hour)) - }) -}) - -var _ = ginkgo.Describe("Report date range", func() { - ginkgo.It("From is set from sinceTime", func() { - opts := Options{Since: 48 * time.Hour}.WithDefaults() - sinceTime := time.Now().Add(-opts.Since) - - report := &api.CatalogReport{ - From: sinceTime.Format(time.RFC3339), - } - - parsed, err := time.Parse(time.RFC3339, report.From) - Expect(err).ToNot(HaveOccurred()) - Expect(parsed).To(BeTemporally("~", time.Now().Add(-48*time.Hour), 2*time.Second)) - }) - - ginkgo.It("From matches sinceTime for 30-day default", func() { - opts := Options{}.WithDefaults() - sinceTime := time.Now().Add(-opts.Since) - - report := &api.CatalogReport{ - From: sinceTime.Format(time.RFC3339), - } - - parsed, err := time.Parse(time.RFC3339, report.From) - Expect(err).ToNot(HaveOccurred()) - Expect(parsed).To(BeTemporally("~", time.Now().Add(-30*24*time.Hour), 2*time.Second)) - }) - - ginkgo.It("query FromTime matches report From", func() { - opts := Options{Since: 7 * 24 * time.Hour}.WithDefaults() - sinceTime := time.Now().Add(-opts.Since) - - report := &api.CatalogReport{ - From: sinceTime.Format(time.RFC3339), - } - - reportFrom, err := time.Parse(time.RFC3339, report.From) - Expect(err).ToNot(HaveOccurred()) - Expect(reportFrom).To(BeTemporally("~", sinceTime, time.Second)) - }) -}) diff --git a/cmd/catalog_get_test.go b/cmd/catalog_get_test.go index 9202b0d50..a59533cd7 100644 --- a/cmd/catalog_get_test.go +++ b/cmd/catalog_get_test.go @@ -32,7 +32,7 @@ var _ = ginkgo.Describe("buildCatalogGetOutput", func() { c.Config = &configJSON r := CatalogGetResult{ ConfigItem: *c, - since: since, + since: since.String(), Related: []query.RelatedConfig{ {ID: uuid.New(), Name: "my-pod", Type: "Kubernetes::Pod", Relation: "outgoing", Health: lo.ToPtr(models.HealthHealthy)}, }, @@ -60,7 +60,7 @@ var _ = ginkgo.Describe("buildCatalogGetOutput", func() { }) ginkgo.It("omits empty sections but always includes header and details", func() { - r := CatalogGetResult{ConfigItem: *makeConfig(), since: since} + r := CatalogGetResult{ConfigItem: *makeConfig(), since: since.String()} out := r.Pretty().String() Expect(out).To(ContainSubstring("my-deployment")) Expect(out).NotTo(ContainSubstring("Relationships")) @@ -71,7 +71,7 @@ var _ = ginkgo.Describe("buildCatalogGetOutput", func() { configJSON := `{"foo":"bar"}` c := makeConfig() c.Config = &configJSON - r := CatalogGetResult{ConfigItem: *c, since: since} + r := CatalogGetResult{ConfigItem: *c, since: since.String()} out := r.Pretty().String() Expect(out).To(ContainSubstring("Config")) Expect(out).To(ContainSubstring("foo")) diff --git a/cmd/catalog_report.go b/cmd/catalog_report.go index ca6d46eee..4a66ddb12 100644 --- a/cmd/catalog_report.go +++ b/cmd/catalog_report.go @@ -13,8 +13,8 @@ import ( "github.com/spf13/cobra" "github.com/flanksource/incident-commander/api" - "github.com/flanksource/incident-commander/catalog_report" "github.com/flanksource/incident-commander/report" + "github.com/flanksource/incident-commander/report/catalog" ) var ( @@ -34,6 +34,9 @@ var ( catalogReportGroupBy string catalogReportChangeArtifacts bool catalogReportAudit bool + catalogReportLimit int + catalogReportMaxItems int + catalogReportMaxChanges int ) var CatalogReportCmd = &cobra.Command{ @@ -78,12 +81,12 @@ Examples: } } - configs, err := resolveConfigs(ctx, queryArgs, 0) + configs, err := resolveConfigs(ctx, queryArgs, catalogReportLimit) if err != nil { return err } - result, err := catalog_report.Export(ctx, configs, opts, catalogReportFormat) + result, err := catalog.Export(ctx, configs, opts, catalogReportFormat) if err != nil { shutdown.ShutdownAndExit(1, err.Error()) return err @@ -121,13 +124,16 @@ Examples: }, } -func buildCatalogReportOptions() catalog_report.Options { - opts := catalog_report.Options{ +func buildCatalogReportOptions() catalog.Options { + opts := catalog.Options{ Title: catalogReportTitle, Recursive: catalogReportRecursive, GroupBy: catalogReportGroupBy, ChangeArtifacts: catalogReportChangeArtifacts, Audit: catalogReportAudit, + Limit: catalogReportLimit, + MaxItems: catalogReportMaxItems, + MaxChanges: catalogReportMaxChanges, Sections: api.CatalogReportSections{ Changes: catalogReportChanges, Insights: catalogReportInsights, @@ -144,14 +150,12 @@ func buildCatalogReportOptions() catalog_report.Options { } } - if catalogReportSettings != "" { - settings, err := catalog_report.LoadSettings(catalogReportSettings) - if err != nil { - logger.Fatalf("failed to load settings: %v", err) - } - opts.Settings = settings - opts.SettingsPath = catalogReportSettings + settings, settingsSource, err := catalog.ResolveSettings(catalogReportSettings) + if err != nil { + logger.Fatalf("failed to load settings: %v", err) } + opts.Settings = settings + opts.SettingsPath = settingsSource return opts } @@ -166,6 +170,9 @@ func init() { CatalogReportCmd.Flags().BoolVar(&catalogReportRecursive, "recursive", false, "Include all descendant config items") CatalogReportCmd.Flags().StringVar(&catalogReportGroupBy, "group-by", "merged", "Group descendant data: 'merged' or 'config'") CatalogReportCmd.Flags().BoolVar(&catalogReportChangeArtifacts, "change-artifacts", false, "Embed change artifacts (images/screenshots) in the report") + CatalogReportCmd.Flags().IntVar(&catalogReportLimit, "limit", 50, "Maximum number of config items to report on, including recursive descendants (0 = unlimited)") + CatalogReportCmd.Flags().IntVar(&catalogReportMaxItems, "max-items", 50, "Maximum items per section (changes, analyses, access, access-logs). Section-specific flags override this. (0 = unlimited)") + CatalogReportCmd.Flags().IntVar(&catalogReportMaxChanges, "max-changes", 100, "Maximum changes per entry, overrides --max-items for the changes section (0 = unlimited)") CatalogReportCmd.Flags().BoolVar(&catalogReportChanges, "changes", true, "Include config changes section") CatalogReportCmd.Flags().BoolVar(&catalogReportInsights, "insights", true, "Include config insights section") CatalogReportCmd.Flags().BoolVar(&catalogReportRelationships, "relationships", true, "Include relationships section") diff --git a/cmd/view.go b/cmd/view.go index 07a5d85a2..ff8df9a3f 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -110,6 +110,6 @@ func init() { ViewRun.Flags().StringVarP(&viewFormat, "format", "f", "json", "Output format: json, csv, html, pdf, facet-html, facet-pdf") ViewRun.Flags().StringVarP(&viewOutFile, "out-file", "o", "", "Write output to file instead of stdout") ViewRun.Flags().StringSliceVar(&viewVars, "var", nil, "Template variables as key=value pairs") - ViewRun.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory with TSX report files (overrides embedded reports)") + ViewRun.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory or TSX file for report rendering (overrides embedded reports)") ViewCmd.AddCommand(ViewRun) } diff --git a/db/rbac_test.go b/db/rbac_test.go new file mode 100644 index 000000000..59079ad49 --- /dev/null +++ b/db/rbac_test.go @@ -0,0 +1,68 @@ +package db + +import ( + "github.com/flanksource/duty/tests/fixtures/dummy" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetGroupMembersForConfigs", func() { + It("returns members of a group referenced by config_access", func() { + rows, err := GetGroupMembersForConfigs(DefaultContext, []uuid.UUID{dummy.MissionControlNamespace.ID}) + Expect(err).ToNot(HaveOccurred()) + + // The MissionControlNamespace config has access rows for both the + // admins group (JohnDoe + Alice) and the readers group (Bob + Charlie). + userIDsByGroup := map[uuid.UUID]map[uuid.UUID]bool{} + groupNames := map[uuid.UUID]string{} + for _, r := range rows { + if userIDsByGroup[r.GroupID] == nil { + userIDsByGroup[r.GroupID] = map[uuid.UUID]bool{} + } + userIDsByGroup[r.GroupID][r.UserID] = true + groupNames[r.GroupID] = r.GroupName + } + + Expect(groupNames).To(HaveKey(dummy.MissionControlAdminsGroup.ID)) + Expect(groupNames[dummy.MissionControlAdminsGroup.ID]).To(Equal("mission-control-admins")) + Expect(userIDsByGroup[dummy.MissionControlAdminsGroup.ID]).To(HaveKey(dummy.JohnDoeExternalUser.ID)) + Expect(userIDsByGroup[dummy.MissionControlAdminsGroup.ID]).To(HaveKey(dummy.AliceExternalUser.ID)) + + Expect(groupNames).To(HaveKey(dummy.MissionControlReadersGroup.ID)) + Expect(userIDsByGroup[dummy.MissionControlReadersGroup.ID]).To(HaveKey(dummy.BobExternalUser.ID)) + Expect(userIDsByGroup[dummy.MissionControlReadersGroup.ID]).To(HaveKey(dummy.CharlieExternalUser.ID)) + }) + + It("returns no rows for configs without group-based access", func() { + // Use a random config ID that has no config_access group rows. + rows, err := GetGroupMembersForConfigs(DefaultContext, []uuid.UUID{uuid.New()}) + Expect(err).ToNot(HaveOccurred()) + Expect(rows).To(BeEmpty()) + }) + + It("returns nil when no config IDs are provided", func() { + rows, err := GetGroupMembersForConfigs(DefaultContext, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(rows).To(BeNil()) + }) + + It("populates member identity fields from external_users", func() { + rows, err := GetGroupMembersForConfigs(DefaultContext, []uuid.UUID{dummy.MissionControlNamespace.ID}) + Expect(err).ToNot(HaveOccurred()) + + var johnRow *GroupMemberRow + for i := range rows { + if rows[i].UserID == dummy.JohnDoeExternalUser.ID { + johnRow = &rows[i] + break + } + } + Expect(johnRow).ToNot(BeNil(), "expected john doe in admins group") + Expect(johnRow.UserName).To(Equal("John Doe")) + Expect(johnRow.Email).To(Equal("johndoe@flanksource.com")) + Expect(johnRow.UserType).To(Equal("user")) + Expect(johnRow.GroupType).To(Equal("group")) + Expect(johnRow.MembershipAddedAt).ToNot(BeZero()) + }) +}) diff --git a/rbac_report/render_facet.go b/rbac_report/render_facet.go index 33bef171d..3b6b71a6b 100644 --- a/rbac_report/render_facet.go +++ b/rbac_report/render_facet.go @@ -36,8 +36,8 @@ func renderWithFacet(ctx context.Context, r *api.RBACReport, format string, view return nil, err } - ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result)/1024, format) - return result, nil + ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result.Data)/1024, format) + return result.Data, nil } func initSlices(r *api.RBACReport) api.RBACReport { diff --git a/report/catalog/change_mappings.go b/report/catalog/change_mappings.go new file mode 100644 index 000000000..94962f31f --- /dev/null +++ b/report/catalog/change_mappings.go @@ -0,0 +1,308 @@ +package catalog + +import ( + "fmt" + "reflect" + + dutyContext "github.com/flanksource/duty/context" + reportAPI "github.com/flanksource/incident-commander/api" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" +) + +type changeMapper struct { + mappings []compiledCategoryMapping +} + +type compiledCategoryMapping struct { + mapping reportAPI.CatalogReportCategoryMapping + filter cel.Program + transform cel.Program +} + +func newChangeMapper(ctx dutyContext.Context, mappings []reportAPI.CatalogReportCategoryMapping) (*changeMapper, error) { + if len(mappings) == 0 { + return nil, nil + } + + envOptions := []cel.EnvOption{ + cel.Variable("change", cel.MapType(cel.StringType, cel.DynType)), + cel.Variable("details", cel.MapType(cel.StringType, cel.DynType)), + cel.Variable("typedChange", cel.MapType(cel.StringType, cel.DynType)), + cel.Variable("artifacts", cel.ListType(cel.DynType)), + cel.Variable("id", cel.StringType), + cel.Variable("configID", cel.StringType), + cel.Variable("configName", cel.StringType), + cel.Variable("configType", cel.StringType), + cel.Variable("permalink", cel.StringType), + cel.Variable("changeType", cel.StringType), + cel.Variable("category", cel.StringType), + cel.Variable("severity", cel.StringType), + cel.Variable("source", cel.StringType), + cel.Variable("summary", cel.StringType), + cel.Variable("createdBy", cel.StringType), + cel.Variable("externalCreatedBy", cel.StringType), + cel.Variable("createdAt", cel.StringType), + cel.Variable("count", cel.IntType), + } + + for _, fn := range dutyContext.CelEnvFuncs { + envOptions = append(envOptions, fn(ctx)) + } + + env, err := cel.NewEnv(envOptions...) + if err != nil { + return nil, err + } + + compiled := make([]compiledCategoryMapping, 0, len(mappings)) + for i, mapping := range mappings { + if mapping.Filter == "" { + return nil, fmt.Errorf("categoryMappings[%d] filter is required", i) + } + if mapping.Category == "" && mapping.Transform == "" { + return nil, fmt.Errorf("categoryMappings[%d] must define category or transform", i) + } + + filter, err := compileChangeMappingProgram(env, mapping.Filter) + if err != nil { + return nil, fmt.Errorf("failed to compile categoryMappings[%d] filter: %w", i, err) + } + + var transform cel.Program + if mapping.Transform != "" { + transform, err = compileChangeMappingProgram(env, mapping.Transform) + if err != nil { + return nil, fmt.Errorf("failed to compile categoryMappings[%d] transform: %w", i, err) + } + } + + compiled = append(compiled, compiledCategoryMapping{ + mapping: mapping, + filter: filter, + transform: transform, + }) + } + + return &changeMapper{mappings: compiled}, nil +} + +func compileChangeMappingProgram(env *cel.Env, expression string) (cel.Program, error) { + ast, issues := env.Compile(expression) + if issues.Err() != nil { + return nil, issues.Err() + } + + return env.Program(ast) +} + +func (m *changeMapper) Apply(change *reportAPI.CatalogReportChange) error { + if change == nil { + return nil + } + + env := changeMappingEnv(change) + category := change.Category + typedChange := change.TypedChange + + if m != nil { + for _, mapping := range m.mappings { + matched, err := evalMappingFilter(mapping.filter, env) + if err != nil { + return fmt.Errorf("failed to evaluate filter %q: %w", mapping.mapping.Filter, err) + } + if !matched { + continue + } + + if category == "" && mapping.mapping.Category != "" { + category = mapping.mapping.Category + env["category"] = category + changeEnv := env["change"].(map[string]any) + changeEnv["category"] = category + } + + if typedChange == nil && mapping.transform != nil { + typedChange, err = evalMappingTransform(mapping.transform, env) + if err != nil { + return fmt.Errorf("failed to evaluate transform %q: %w", mapping.mapping.Transform, err) + } + if typedChange != nil { + env["typedChange"] = typedChange + changeEnv := env["change"].(map[string]any) + changeEnv["typedChange"] = typedChange + } + } + + if category != "" && typedChange != nil { + break + } + } + } + + if change.Category == "" { + change.Category = category + } + if change.TypedChange == nil { + if typedChange != nil { + change.TypedChange = typedChange + } else { + change.TypedChange = typedChangeFromDetails(change.Details) + } + } + + return nil +} + +func evalMappingFilter(program cel.Program, env map[string]any) (bool, error) { + out, _, err := program.Eval(env) + if err != nil { + return false, err + } + + value, ok := celValueToNative(out).(bool) + if !ok { + return false, fmt.Errorf("filter returned %T, expected bool", celValueToNative(out)) + } + + return value, nil +} + +func evalMappingTransform(program cel.Program, env map[string]any) (map[string]any, error) { + out, _, err := program.Eval(env) + if err != nil { + return nil, err + } + + value, ok := celValueToNative(out).(map[string]any) + if !ok { + return nil, nil + } + + kind, _ := value["kind"].(string) + if kind == "" { + return nil, nil + } + + return value, nil +} + +func typedChangeFromDetails(details map[string]any) map[string]any { + if len(details) == 0 { + return nil + } + + kind, _ := details["kind"].(string) + if kind == "" { + return nil + } + + return celValueToNative(details).(map[string]any) +} + +func changeMappingEnv(change *reportAPI.CatalogReportChange) map[string]any { + details := map[string]any{} + if change.Details != nil { + details = celValueToNative(change.Details).(map[string]any) + } + + typedChange := map[string]any{} + if change.TypedChange != nil { + typedChange = celValueToNative(change.TypedChange).(map[string]any) + } + + artifacts := make([]any, 0, len(change.Artifacts)) + for _, artifact := range change.Artifacts { + artifacts = append(artifacts, map[string]any{ + "id": artifact.ID, + "filename": artifact.Filename, + "contentType": artifact.ContentType, + "size": artifact.Size, + "dataUri": artifact.DataURI, + }) + } + + changeEnv := map[string]any{ + "id": change.ID, + "configID": change.ConfigID, + "configName": change.ConfigName, + "configType": change.ConfigType, + "permalink": change.Permalink, + "changeType": change.ChangeType, + "category": change.Category, + "severity": change.Severity, + "source": change.Source, + "summary": change.Summary, + "details": details, + "typedChange": typedChange, + "createdBy": change.CreatedBy, + "externalCreatedBy": change.ExternalCreatedBy, + "createdAt": change.CreatedAt, + "count": int64(change.Count), + "artifacts": artifacts, + } + + return map[string]any{ + "change": changeEnv, + "details": details, + "typedChange": typedChange, + "artifacts": artifacts, + "id": change.ID, + "configID": change.ConfigID, + "configName": change.ConfigName, + "configType": change.ConfigType, + "permalink": change.Permalink, + "changeType": change.ChangeType, + "category": change.Category, + "severity": change.Severity, + "source": change.Source, + "summary": change.Summary, + "createdBy": change.CreatedBy, + "externalCreatedBy": change.ExternalCreatedBy, + "createdAt": change.CreatedAt, + "count": int64(change.Count), + } +} + +func celValueToNative(value any) any { + switch v := value.(type) { + case nil, bool, string, int, int32, int64, uint, uint32, uint64, float32, float64: + return v + case map[string]any: + out := make(map[string]any, len(v)) + for key, item := range v { + out[key] = celValueToNative(item) + } + return out + case []any: + out := make([]any, 0, len(v)) + for _, item := range v { + out = append(out, celValueToNative(item)) + } + return out + case ref.Val: + return celValueToNative(v.Value()) + } + + rv := reflect.ValueOf(value) + if !rv.IsValid() { + return nil + } + + switch rv.Kind() { + case reflect.Map: + out := map[string]any{} + for _, key := range rv.MapKeys() { + out[fmt.Sprint(celValueToNative(key.Interface()))] = celValueToNative(rv.MapIndex(key).Interface()) + } + return out + case reflect.Slice, reflect.Array: + out := make([]any, 0, rv.Len()) + for i := 0; i < rv.Len(); i++ { + out = append(out, celValueToNative(rv.Index(i).Interface())) + } + return out + } + + return value +} diff --git a/report/catalog/change_mappings_test.go b/report/catalog/change_mappings_test.go new file mode 100644 index 000000000..527d4214b --- /dev/null +++ b/report/catalog/change_mappings_test.go @@ -0,0 +1,100 @@ +package catalog + +import ( + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + dutyContext "github.com/flanksource/duty/context" + "github.com/flanksource/duty/query" + "github.com/flanksource/incident-commander/api" +) + +var _ = ginkgo.Describe("ChangeMappings", func() { + var ctx dutyContext.Context + + ginkgo.BeforeEach(func() { + ctx = dutyContext.New() + }) + + ginkgo.It("applies category and transform independently", func() { + mapper, err := newChangeMapper(ctx, []api.CatalogReportCategoryMapping{ + { + Category: "backup.failed", + Filter: `changeType == "BackupFailed"`, + }, + { + Filter: `"kind" in details && details["kind"] == "Backup/v1"`, + Transform: `details`, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + change := api.CatalogReportChange{ + ChangeType: "BackupFailed", + Details: map[string]any{ + "kind": "Backup/v1", + "status": "failed", + "target": "prod-db", + }, + } + + Expect(mapper.Apply(&change)).To(Succeed()) + Expect(change.Category).To(Equal("backup.failed")) + Expect(change.TypedChange).To(HaveKeyWithValue("kind", "Backup/v1")) + Expect(change.TypedChange).To(HaveKeyWithValue("target", "prod-db")) + }) + + ginkgo.It("ignores transform results without a kind", func() { + mapper, err := newChangeMapper(ctx, []api.CatalogReportCategoryMapping{ + { + Filter: `changeType == "ScalingReplicaSet"`, + Transform: `{"from_replicas": 1, "to_replicas": 3}`, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + change := api.CatalogReportChange{ChangeType: "ScalingReplicaSet"} + Expect(mapper.Apply(&change)).To(Succeed()) + Expect(change.TypedChange).To(BeNil()) + }) + + ginkgo.It("hydrates typedChange from typed details without a transform rule", func() { + var mapper *changeMapper + change := api.CatalogReportChange{ + ChangeType: "PipelineRunCompleted", + Details: map[string]any{ + "kind": "PipelineRun/v1", + "pipeline_name": "deploy-api", + "status": "completed", + }, + } + + Expect(mapper.Apply(&change)).To(Succeed()) + Expect(change.TypedChange).To(HaveKeyWithValue("kind", "PipelineRun/v1")) + Expect(change.TypedChange).To(HaveKeyWithValue("pipeline_name", "deploy-api")) + }) + + ginkgo.It("threads decoded details onto report changes", func() { + change := newCatalogReportChangeFromRow(queryChange("chg-1", "BackupCompleted"), "prod-db", "AWS::RDS::DBInstance", map[string]any{ + "kind": "Backup/v1", + "status": "completed", + }) + + Expect(change.ConfigName).To(Equal("prod-db")) + Expect(change.ConfigType).To(Equal("AWS::RDS::DBInstance")) + Expect(change.Details).To(HaveKeyWithValue("kind", "Backup/v1")) + Expect(change.Details).To(HaveKeyWithValue("status", "completed")) + }) +}) + +func queryChange(id, changeType string) query.ConfigChangeRow { + return query.ConfigChangeRow{ + ID: id, + ConfigID: "cfg-1", + ChangeType: changeType, + Severity: "info", + Source: "unit-test", + Summary: "summary", + Count: 1, + } +} diff --git a/report/catalog/default-settings.yaml b/report/catalog/default-settings.yaml new file mode 100644 index 000000000..6e50ae1ef --- /dev/null +++ b/report/catalog/default-settings.yaml @@ -0,0 +1,34 @@ +filters: + - "type!=Kubernetes::ConfigMap" + - "type!=Kubernetes::Secret" + - "type!=Kubernetes::Event" + +thresholds: + staleDays: 90 + reviewOverdueDays: 90 + +categoryMappings: + - category: rbac.granted + filter: 'changeType == "PermissionGranted" || changeType == "PermissionAdded" || changeType == "IAMRoleAdded"' + - category: rbac.revoked + filter: 'changeType == "PermissionRevoked" || changeType == "PermissionRemoved" || changeType == "IAMRoleRemoved"' + - category: backup.success + filter: 'changeType == "BackupCompleted" || changeType == "BackupSuccessful" || (changeType == "BACKUP_DB" && severity == "low") || (("kind" in details && details["kind"] == "Backup/v1") && ("status" in details && (details["status"] == "success" || details["status"] == "successful" || details["status"] == "completed")))' + - category: backup.failed + filter: 'changeType == "BackupFailed" || (changeType == "BACKUP_DB" && severity == "high") || (("kind" in details && details["kind"] == "Backup/v1") && ("status" in details && (details["status"] == "failed" || details["status"] == "error")))' + - category: backup.success + filter: 'changeType == "BACKUP_DB" && severity != "high"' + - category: backup.progress + filter: 'changeType == "BackupStarted" || changeType == "BackupRunning" || changeType == "BackupEnqueued" || (("kind" in details && details["kind"] == "Backup/v1") && ("status" in details && (details["status"] == "started" || details["status"] == "running" || details["status"] == "enqueued" || details["status"] == "progress")))' + - category: backup.restore + filter: 'changeType == "BackupRestored" || changeType == "RestoreCompleted" || (("kind" in details && details["kind"] == "Backup/v1") && ("status" in details && (details["status"] == "restored" || details["status"] == "restore_completed")))' + - category: deployment.spec + filter: 'changeType == "diff" || ("kind" in details && details["kind"] == "Deployment/v1")' + - category: deployment.success + filter: 'changeType == "CodeDeployment" && severity == "info"' + - category: deployment.failed + filter: 'changeType == "CodeDeployment" && severity == "failed"' + - category: deployment.scale + filter: 'changeType == "ScalingReplicaSet" || ("kind" in details && details["kind"] == "Scaling/v1")' + - category: deployment.policy + filter: 'changeType == "PolicyUpdate"' diff --git a/catalog_report/export.go b/report/catalog/export.go similarity index 69% rename from catalog_report/export.go rename to report/catalog/export.go index fd025700b..67fca7709 100644 --- a/catalog_report/export.go +++ b/report/catalog/export.go @@ -1,10 +1,11 @@ -package catalog_report +package catalog import ( "encoding/json" "os/exec" "sort" "strings" + "time" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" @@ -12,7 +13,8 @@ import ( "github.com/google/uuid" "github.com/flanksource/incident-commander/api" - "github.com/flanksource/incident-commander/scraper_report" + "github.com/flanksource/incident-commander/db" + "github.com/flanksource/incident-commander/report/scraper" ) type ExportResult struct { @@ -38,7 +40,7 @@ func Export(ctx context.Context, configs []models.ConfigItem, opts Options, form len(r.Entries), len(r.Changes), len(r.Analyses)) if opts.Audit { - r.Audit = buildAudit(ctx, opts, scraperIDs, queryLog) + r.Audit = buildAudit(ctx, opts, configs, scraperIDs, queryLog) } result := &ExportResult{} @@ -58,7 +60,7 @@ func Export(ctx context.Context, configs []models.ConfigItem, opts Options, form return result, err } -func buildAudit(ctx context.Context, opts Options, scraperIDs []string, queryLog *query.QueryLog) *api.CatalogReportAudit { +func buildAudit(ctx context.Context, opts Options, configs []models.ConfigItem, scraperIDs []string, queryLog *query.QueryLog) *api.CatalogReportAudit { audit := &api.CatalogReportAudit{ BuildCommit: api.BuildCommit, BuildVersion: api.BuildVersion, @@ -72,6 +74,7 @@ func buildAudit(ctx context.Context, opts Options, scraperIDs []string, queryLog }, Scrapers: []api.ScraperInfo{}, Queries: []api.CatalogReportQuery{}, + Groups: []api.CatalogReportGroup{}, } if opts.Settings != nil { @@ -107,7 +110,7 @@ func buildAudit(ctx context.Context, opts Options, scraperIDs []string, queryLog if err != nil { continue } - info, err := scraper_report.BuildScraperInfo(ctx, id) + info, err := scraper.BuildScraperInfo(ctx, id) if err != nil { ctx.Logger.V(2).Infof("failed to build scraper info for %s: %v", sid, err) continue @@ -115,9 +118,69 @@ func buildAudit(ctx context.Context, opts Options, scraperIDs []string, queryLog audit.Scrapers = append(audit.Scrapers, *info) } + audit.Groups = buildAuditGroups(ctx, configs) + return audit } +func buildAuditGroups(ctx context.Context, configs []models.ConfigItem) []api.CatalogReportGroup { + if len(configs) == 0 { + return []api.CatalogReportGroup{} + } + + configIDs := make([]uuid.UUID, 0, len(configs)) + for _, c := range configs { + configIDs = append(configIDs, c.ID) + } + + rows, err := db.GetGroupMembersForConfigs(ctx, configIDs) + if err != nil { + ctx.Logger.V(2).Infof("failed to load group members for audit: %v", err) + return []api.CatalogReportGroup{} + } + + // Preserve the SQL ORDER BY (group_name, then deleted-last, then user_name) + // by accumulating in first-seen order. + byID := map[uuid.UUID]*api.CatalogReportGroup{} + order := []uuid.UUID{} + for _, r := range rows { + g, ok := byID[r.GroupID] + if !ok { + g = &api.CatalogReportGroup{ + ID: r.GroupID.String(), + Name: r.GroupName, + GroupType: r.GroupType, + Members: []api.CatalogReportGroupMember{}, + } + byID[r.GroupID] = g + order = append(order, r.GroupID) + } + + member := api.CatalogReportGroupMember{ + UserID: r.UserID.String(), + Name: r.UserName, + Email: r.Email, + UserType: r.UserType, + MembershipAddedAt: r.MembershipAddedAt.Format(time.RFC3339), + } + if r.LastSignedInAt != nil { + s := r.LastSignedInAt.Format(time.RFC3339) + member.LastSignedInAt = &s + } + if r.MembershipDeletedAt != nil { + s := r.MembershipDeletedAt.Format(time.RFC3339) + member.MembershipDeletedAt = &s + } + g.Members = append(g.Members, member) + } + + out := make([]api.CatalogReportGroup, 0, len(order)) + for _, id := range order { + out = append(out, *byID[id]) + } + return out +} + func gitStatus() string { out, err := exec.Command("git", "status", "--short").Output() if err != nil { @@ -193,6 +256,9 @@ func initSlices(r *api.CatalogReport) api.CatalogReport { if out.Audit.Queries == nil { out.Audit.Queries = []api.CatalogReportQuery{} } + if out.Audit.Groups == nil { + out.Audit.Groups = []api.CatalogReportGroup{} + } } return out } diff --git a/catalog_report/render_facet.go b/report/catalog/render_facet.go similarity index 97% rename from catalog_report/render_facet.go rename to report/catalog/render_facet.go index 9e3000bdc..102988d4b 100644 --- a/catalog_report/render_facet.go +++ b/report/catalog/render_facet.go @@ -1,4 +1,4 @@ -package catalog_report +package catalog import ( "fmt" diff --git a/catalog_report/report.go b/report/catalog/report.go similarity index 71% rename from catalog_report/report.go rename to report/catalog/report.go index 1c4ef8087..f1eb2eeaa 100644 --- a/catalog_report/report.go +++ b/report/catalog/report.go @@ -1,8 +1,10 @@ -package catalog_report +package catalog import ( "encoding/base64" + "encoding/json" "fmt" + "math" "slices" "strings" "time" @@ -10,6 +12,7 @@ import ( "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" "github.com/flanksource/duty/query" + dutyTypes "github.com/flanksource/duty/types" "github.com/google/uuid" "github.com/samber/lo" @@ -27,6 +30,42 @@ type Options struct { Audit bool Settings *Settings SettingsPath string + + // Limit caps the number of config items (including recursive descendants) + // included in the report. 0 = unlimited. + Limit int + // MaxItems caps each per-entry section (changes, analyses, access, + // access-logs). Section-specific overrides take precedence. 0 = unlimited. + MaxItems int + // MaxChanges overrides MaxItems for the changes section. 0 = unlimited. + MaxChanges int +} + +// effectiveMax resolves the cap for a section, taking the tighter of an +// optional section-specific override and the generic MaxItems floor. A return +// of 0 means "no cap". +func (o Options) effectiveMax(override int) int { + switch { + case override > 0 && o.MaxItems > 0: + if override < o.MaxItems { + return override + } + return o.MaxItems + case override > 0: + return override + default: + return o.MaxItems + } +} + +// pageSizeFor converts an effectiveMax result into a duty PageSize value. +// duty's BaseCatalogSearch.SetDefaults forces PageSize<=0 to 50, so "unlimited" +// must be expressed as a large sentinel. +func (o Options) pageSizeFor(override int) int { + if n := o.effectiveMax(override); n > 0 { + return n + } + return math.MaxInt32 } func (o Options) StaleDays() int { @@ -62,6 +101,14 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) } opts = opts.WithDefaults() sinceTime := time.Now().Add(-opts.Since) + var mappings []api.CatalogReportCategoryMapping + if opts.Settings != nil { + mappings = opts.Settings.CategoryMappings + } + mapper, err := newChangeMapper(ctx, mappings) + if err != nil { + return nil, nil, fmt.Errorf("failed to initialize change mappings: %w", err) + } report := &api.CatalogReport{ Title: opts.Title, @@ -76,6 +123,10 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) report.Parents = resolveParents(ctx, &configs[0]) + if opts.Limit > 0 && len(configs) > opts.Limit { + configs = configs[:opts.Limit] + } + scraperIDSet := make(map[string]bool) for _, config := range configs { if config.ScraperID != nil && *config.ScraperID != "" { @@ -84,7 +135,7 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) } for _, config := range configs { - entry, entryScraperIDs, err := buildEntry(ctx, &config, opts, sinceTime) + entry, entryScraperIDs, err := buildEntryWithMapper(ctx, &config, opts, sinceTime, mapper) if err != nil { return nil, nil, fmt.Errorf("failed to build entry for %s: %w", config.GetName(), err) } @@ -130,6 +181,10 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) } func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time) (*api.CatalogReportEntry, []string, error) { + return buildEntryWithMapper(ctx, config, opts, sinceTime, nil) +} + +func buildEntryWithMapper(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time, mapper *changeMapper) (*api.CatalogReportEntry, []string, error) { entry := &api.CatalogReportEntry{ ConfigItem: api.NewCatalogReportConfigItem(*config), } @@ -178,35 +233,25 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si BaseCatalogSearch: query.BaseCatalogSearch{ CatalogID: catalogIDsCSV, FromTime: &sinceTime, - PageSize: 500, + PageSize: opts.pageSizeFor(opts.MaxChanges), }, }) if err != nil { return nil, nil, fmt.Errorf("failed to get changes: %w", err) } - entry.Changes = lo.Map(resp.Changes, func(c query.ConfigChangeRow, _ int) api.CatalogReportChange { + detailsByID, err := loadCatalogChangeDetails(ctx, resp.Changes) + if err != nil { + return nil, nil, fmt.Errorf("failed to load change details: %w", err) + } + entry.Changes = make([]api.CatalogReportChange, 0, len(resp.Changes)) + for _, c := range resp.Changes { name, typ := configMeta(c.ConfigID) - r := api.CatalogReportChange{ - ID: c.ID, - ConfigID: c.ConfigID, - ConfigName: name, - ConfigType: typ, - Permalink: api.ConfigPermalink(c.ConfigID), - ChangeType: c.ChangeType, - Severity: c.Severity, - Source: c.Source, - Summary: c.Summary, - ExternalCreatedBy: c.ExternalCreatedBy, - Count: c.Count, + r := newCatalogReportChangeFromRow(c, name, typ, detailsByID[c.ID]) + if err := mapper.Apply(&r); err != nil { + return nil, nil, fmt.Errorf("failed to apply change mappings for %s: %w", c.ID, err) } - if c.CreatedAt != nil { - r.CreatedAt = c.CreatedAt.Format(time.RFC3339) - } - if c.CreatedBy != nil { - r.CreatedBy = c.CreatedBy.String() - } - return r - }) + entry.Changes = append(entry.Changes, r) + } entry.ChangeCount = len(entry.Changes) if opts.ChangeArtifacts && len(entry.Changes) > 0 { @@ -218,7 +263,7 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si resp, err := query.FindCatalogInsights(ctx, query.CatalogInsightsSearchRequest{ BaseCatalogSearch: query.BaseCatalogSearch{ CatalogID: catalogIDsCSV, - PageSize: 500, + PageSize: opts.pageSizeFor(0), }, }) if err != nil { @@ -236,6 +281,9 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si if err != nil { return nil, nil, fmt.Errorf("failed to get access: %w", err) } + if limit := opts.effectiveMax(0); limit > 0 && len(rbacRows) > limit { + rbacRows = rbacRows[:limit] + } entry.RBACResources = groupRBACByConfig(rbacRows, configMap, opts) for _, r := range entry.RBACResources { entry.AccessCount += len(r.Users) @@ -247,6 +295,9 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si if err != nil { return nil, nil, fmt.Errorf("failed to get access logs: %w", err) } + if limit := opts.effectiveMax(0); limit > 0 && len(logs) > limit { + logs = logs[:limit] + } entry.AccessLogs = lo.Map(logs, func(l accessLogRow, _ int) api.CatalogReportAccessLog { return newAccessLogEntry(l) }) @@ -259,6 +310,74 @@ func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, si return entry, scraperIDs, nil } +func newCatalogReportChangeFromRow(c query.ConfigChangeRow, configName, configType string, details map[string]any) api.CatalogReportChange { + r := api.CatalogReportChange{ + ID: c.ID, + ConfigID: c.ConfigID, + ConfigName: configName, + ConfigType: configType, + Permalink: api.ConfigPermalink(c.ConfigID), + ChangeType: c.ChangeType, + Severity: c.Severity, + Source: c.Source, + Summary: c.Summary, + Details: details, + ExternalCreatedBy: c.ExternalCreatedBy, + Count: c.Count, + } + if c.CreatedAt != nil { + r.CreatedAt = c.CreatedAt.Format(time.RFC3339) + } + if c.CreatedBy != nil { + r.CreatedBy = c.CreatedBy.String() + } + return r +} + +func loadCatalogChangeDetails(ctx context.Context, rows []query.ConfigChangeRow) (map[string]map[string]any, error) { + if len(rows) == 0 { + return map[string]map[string]any{}, nil + } + + ids := make([]uuid.UUID, 0, len(rows)) + for _, row := range rows { + id, err := uuid.Parse(row.ID) + if err != nil { + continue + } + ids = append(ids, id) + } + + if len(ids) == 0 { + return map[string]map[string]any{}, nil + } + + changes, err := query.GetCatalogChangesByIDs(ctx, ids) + if err != nil { + return nil, err + } + + detailsByID := make(map[string]map[string]any, len(changes)) + for _, change := range changes { + detailsByID[change.ID.String()] = decodeJSONMap(change.Details) + } + + return detailsByID, nil +} + +func decodeJSONMap(raw dutyTypes.JSON) map[string]any { + if len(raw) == 0 { + return nil + } + + var decoded map[string]any + if err := json.Unmarshal(raw, &decoded); err != nil { + return nil + } + + return decoded +} + func buildConfigGroups(report *api.CatalogReport, configMap map[uuid.UUID]models.ConfigItem) []api.CatalogReportConfigGroup { changesByConfig := lo.GroupBy(report.Changes, func(c api.CatalogReportChange) string { return c.ConfigID }) analysesByConfig := lo.GroupBy(report.Analyses, func(a api.CatalogReportAnalysis) string { return a.ConfigID }) @@ -304,23 +423,52 @@ func sortedConfigIDs(m map[uuid.UUID]models.ConfigItem) []uuid.UUID { return ids } +// resolveParents derives report ancestry from config.Path to avoid recursive +// ParentID walks that can loop forever on cyclic catalog data. func resolveParents(ctx context.Context, config *models.ConfigItem) []models.ConfigItem { - var parents []models.ConfigItem - current := config - for current.ParentID != nil { - loaded, err := query.GetConfigsByIDs(ctx, []uuid.UUID{*current.ParentID}) - if err != nil || len(loaded) == 0 { - break - } - parents = append(parents, loaded[0]) - current = &loaded[0] + parentIDs := parentIDsFromPath(config) + if len(parentIDs) == 0 { + return nil } - for i, j := 0, len(parents)-1; i < j; i, j = i+1, j-1 { - parents[i], parents[j] = parents[j], parents[i] + + loaded, err := query.GetConfigsByIDs(ctx, parentIDs) + if err != nil || len(loaded) == 0 { + return nil } + + byID := make(map[uuid.UUID]models.ConfigItem, len(loaded)) + for _, ci := range loaded { + byID[ci.ID] = ci + } + + parents := make([]models.ConfigItem, 0, len(parentIDs)) + for _, id := range parentIDs { + if ci, ok := byID[id]; ok { + parents = append(parents, ci) + } + } + return parents } +func parentIDsFromPath(config *models.ConfigItem) []uuid.UUID { + if config == nil || config.Path == "" { + return nil + } + + segments := strings.Split(config.Path, ".") + parentIDs := make([]uuid.UUID, 0, len(segments)) + for _, seg := range segments { + id, err := uuid.Parse(seg) + if err != nil || id == config.ID { + continue + } + parentIDs = append(parentIDs, id) + } + + return parentIDs +} + type accessLogRow struct { ConfigID uuid.UUID `gorm:"column:config_id"` ConfigName string `gorm:"column:config_name"` @@ -498,13 +646,21 @@ func groupRBACByConfig(rows []db.RBACAccessRow, configMap map[uuid.UUID]models.C } func configTreeNodeToReport(n *query.ConfigTreeNode) *api.CatalogReportTreeNode { + return buildReportTreeNode(n, make(map[uuid.UUID]bool)) +} + +func buildReportTreeNode(n *query.ConfigTreeNode, visited map[uuid.UUID]bool) *api.CatalogReportTreeNode { result := &api.CatalogReportTreeNode{ CatalogReportConfigItem: api.NewCatalogReportConfigItem(n.ConfigItem), EdgeType: n.EdgeType, Relation: n.Relation, } + if visited[n.ID] { + return result + } + visited[n.ID] = true for _, c := range n.Children { - result.Children = append(result.Children, *configTreeNodeToReport(c)) + result.Children = append(result.Children, *buildReportTreeNode(c, visited)) } return result } diff --git a/report/catalog/report_test.go b/report/catalog/report_test.go new file mode 100644 index 000000000..a30a43992 --- /dev/null +++ b/report/catalog/report_test.go @@ -0,0 +1,185 @@ +package catalog + +import ( + "testing" + "time" + + "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query" + "github.com/google/uuid" + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/flanksource/incident-commander/api" +) + +func TestCatalogReport(t *testing.T) { + RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "CatalogReport") +} + +var _ = ginkgo.Describe("Options", func() { + ginkgo.It("WithDefaults sets 30-day since", func() { + opts := Options{}.WithDefaults() + Expect(opts.Since).To(Equal(30 * 24 * time.Hour)) + }) + + ginkgo.It("WithDefaults preserves custom since", func() { + opts := Options{Since: 7 * 24 * time.Hour}.WithDefaults() + Expect(opts.Since).To(Equal(7 * 24 * time.Hour)) + }) +}) + +var _ = ginkgo.Describe("Report date range", func() { + ginkgo.It("From is set from sinceTime", func() { + opts := Options{Since: 48 * time.Hour}.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + From: sinceTime.Format(time.RFC3339), + } + + parsed, err := time.Parse(time.RFC3339, report.From) + Expect(err).ToNot(HaveOccurred()) + Expect(parsed).To(BeTemporally("~", time.Now().Add(-48*time.Hour), 2*time.Second)) + }) + + ginkgo.It("From matches sinceTime for 30-day default", func() { + opts := Options{}.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + From: sinceTime.Format(time.RFC3339), + } + + parsed, err := time.Parse(time.RFC3339, report.From) + Expect(err).ToNot(HaveOccurred()) + Expect(parsed).To(BeTemporally("~", time.Now().Add(-30*24*time.Hour), 2*time.Second)) + }) + + ginkgo.It("query FromTime matches report From", func() { + opts := Options{Since: 7 * 24 * time.Hour}.WithDefaults() + sinceTime := time.Now().Add(-opts.Since) + + report := &api.CatalogReport{ + From: sinceTime.Format(time.RFC3339), + } + + reportFrom, err := time.Parse(time.RFC3339, report.From) + Expect(err).ToNot(HaveOccurred()) + Expect(reportFrom).To(BeTemporally("~", sinceTime, time.Second)) + }) +}) + +var _ = ginkgo.Describe("Options.effectiveMax", func() { + cases := []struct { + name string + maxItems int + override int + expected int + }{ + {"both unlimited", 0, 0, 0}, + {"only MaxItems", 50, 0, 50}, + {"only override", 0, 100, 100}, + {"override tighter than MaxItems", 50, 20, 20}, + {"override looser than MaxItems", 50, 100, 50}, + {"override equals MaxItems", 50, 50, 50}, + } + for _, tc := range cases { + ginkgo.It(tc.name, func() { + opts := Options{MaxItems: tc.maxItems} + Expect(opts.effectiveMax(tc.override)).To(Equal(tc.expected)) + }) + } +}) + +var _ = ginkgo.Describe("parentIDsFromPath", func() { + ginkgo.It("returns parents in path order", func() { + parentA := uuid.New() + parentB := uuid.New() + child := uuid.New() + + config := &models.ConfigItem{ + ID: child, + Path: parentA.String() + "." + parentB.String() + "." + child.String(), + } + + Expect(parentIDsFromPath(config)).To(Equal([]uuid.UUID{parentA, parentB})) + }) + + ginkgo.It("ignores invalid segments and the config itself", func() { + parent := uuid.New() + child := uuid.New() + + config := &models.ConfigItem{ + ID: child, + Path: "not-a-uuid." + child.String() + "." + parent.String() + ".still-not-a-uuid", + } + + Expect(parentIDsFromPath(config)).To(Equal([]uuid.UUID{parent})) + }) + + ginkgo.It("returns nil for nil config or empty path", func() { + Expect(parentIDsFromPath(nil)).To(BeNil()) + Expect(parentIDsFromPath(&models.ConfigItem{})).To(BeNil()) + }) + + ginkgo.It("uses path only even when ParentID is cyclic", func() { + parentA := uuid.New() + parentB := uuid.New() + cycleParent := uuid.New() + child := uuid.New() + + config := &models.ConfigItem{ + ID: child, + ParentID: &cycleParent, + Path: parentA.String() + "." + parentB.String() + "." + child.String(), + } + + Expect(parentIDsFromPath(config)).To(Equal([]uuid.UUID{parentA, parentB})) + }) +}) + +var _ = ginkgo.Describe("configTreeNodeToReport cycle protection", func() { + ginkgo.It("terminates on a self-referential cycle", func() { + idA := uuid.New() + nodeA := &query.ConfigTreeNode{ + ConfigItem: models.ConfigItem{ID: idA}, + } + // A -> A (self-loop) + nodeA.Children = []*query.ConfigTreeNode{nodeA} + + result := configTreeNodeToReport(nodeA) + Expect(result).ToNot(BeNil()) + Expect(result.Children).To(HaveLen(1)) + Expect(result.Children[0].Children).To(BeEmpty()) + }) + + ginkgo.It("terminates on an A -> B -> A cycle", func() { + idA := uuid.New() + idB := uuid.New() + nodeA := &query.ConfigTreeNode{ConfigItem: models.ConfigItem{ID: idA}} + nodeB := &query.ConfigTreeNode{ConfigItem: models.ConfigItem{ID: idB}} + nodeA.Children = []*query.ConfigTreeNode{nodeB} + nodeB.Children = []*query.ConfigTreeNode{nodeA} + + result := configTreeNodeToReport(nodeA) + Expect(result).ToNot(BeNil()) + Expect(result.Children).To(HaveLen(1)) + // nodeB's child is nodeA again, but A was already visited — empty. + Expect(result.Children[0].Children).To(HaveLen(1)) + Expect(result.Children[0].Children[0].Children).To(BeEmpty()) + }) + + ginkgo.It("preserves acyclic subtrees", func() { + idA, idB, idC := uuid.New(), uuid.New(), uuid.New() + nodeC := &query.ConfigTreeNode{ConfigItem: models.ConfigItem{ID: idC}} + nodeB := &query.ConfigTreeNode{ConfigItem: models.ConfigItem{ID: idB}, Children: []*query.ConfigTreeNode{nodeC}} + nodeA := &query.ConfigTreeNode{ConfigItem: models.ConfigItem{ID: idA}, Children: []*query.ConfigTreeNode{nodeB}} + + result := configTreeNodeToReport(nodeA) + Expect(result.Children).To(HaveLen(1)) + Expect(result.Children[0].Children).To(HaveLen(1)) + Expect(result.Children[0].Children[0].Children).To(BeEmpty()) + }) +}) diff --git a/catalog_report/settings.go b/report/catalog/settings.go similarity index 58% rename from catalog_report/settings.go rename to report/catalog/settings.go index b88a8542d..926809c6a 100644 --- a/catalog_report/settings.go +++ b/report/catalog/settings.go @@ -1,13 +1,13 @@ -package catalog_report +package catalog import ( _ "embed" "fmt" "os" - "sort" "strings" - "github.com/flanksource/clicky/api" + clickyAPI "github.com/flanksource/clicky/api" + reportAPI "github.com/flanksource/incident-commander/api" "sigs.k8s.io/yaml" ) @@ -17,9 +17,9 @@ const EmbeddedSettingsSource = "embedded defaults" var defaultSettingsYAML []byte type Settings struct { - Filters []string `json:"filters,omitempty" yaml:"filters,omitempty"` - Thresholds SettingsThresholds `json:"thresholds,omitempty" yaml:"thresholds,omitempty"` - CategoryMappings map[string][]string `json:"categoryMappings,omitempty" yaml:"categoryMappings,omitempty"` + Filters []string `json:"filters,omitempty" yaml:"filters,omitempty"` + Thresholds SettingsThresholds `json:"thresholds,omitempty" yaml:"thresholds,omitempty"` + CategoryMappings []reportAPI.CatalogReportCategoryMapping `json:"categoryMappings,omitempty" yaml:"categoryMappings,omitempty"` } type SettingsThresholds struct { @@ -41,10 +41,7 @@ func (s *Settings) Clone() *Settings { } if len(s.CategoryMappings) > 0 { - out.CategoryMappings = make(map[string][]string, len(s.CategoryMappings)) - for key, values := range s.CategoryMappings { - out.CategoryMappings[key] = append([]string(nil), values...) - } + out.CategoryMappings = append([]reportAPI.CatalogReportCategoryMapping(nil), s.CategoryMappings...) } return out @@ -93,31 +90,33 @@ func ResolveSettings(path string) (*Settings, string, error) { return settings, fmt.Sprintf("%s + %s", EmbeddedSettingsSource, path), nil } -func (s *Settings) Pretty() api.Text { +func (s *Settings) Pretty() clickyAPI.Text { if s == nil { - return api.Text{Content: "", Style: "text-gray-500"} + return clickyAPI.Text{Content: "", Style: "text-gray-500"} } - items := []api.KeyValuePair{} + items := []clickyAPI.KeyValuePair{} if len(s.Filters) > 0 { - items = append(items, api.KeyValue("Filters", strings.Join(s.Filters, ", "))) + items = append(items, clickyAPI.KeyValue("Filters", strings.Join(s.Filters, ", "))) } if s.Thresholds.StaleDays > 0 || s.Thresholds.ReviewOverdueDays > 0 { - items = append(items, api.KeyValue("Stale", fmt.Sprintf("%dd", s.Thresholds.StaleDays))) - items = append(items, api.KeyValue("Review Overdue", fmt.Sprintf("%dd", s.Thresholds.ReviewOverdueDays))) + items = append(items, clickyAPI.KeyValue("Stale", fmt.Sprintf("%dd", s.Thresholds.StaleDays))) + items = append(items, clickyAPI.KeyValue("Review Overdue", fmt.Sprintf("%dd", s.Thresholds.ReviewOverdueDays))) } if len(s.CategoryMappings) > 0 { - keys := make([]string, 0, len(s.CategoryMappings)) - for k := range s.CategoryMappings { - keys = append(keys, k) - } - sort.Strings(keys) - var cats []string - for _, k := range keys { - cats = append(cats, fmt.Sprintf("%s: %s", k, strings.Join(s.CategoryMappings[k], ", "))) + var mappings []string + for _, mapping := range s.CategoryMappings { + summary := fmt.Sprintf("filter=%s", mapping.Filter) + if mapping.Category != "" { + summary = fmt.Sprintf("category=%s %s", mapping.Category, summary) + } + if mapping.Transform != "" { + summary += fmt.Sprintf(" transform=%s", mapping.Transform) + } + mappings = append(mappings, summary) } - items = append(items, api.KeyValue("Categories", strings.Join(cats, " | "))) + items = append(items, clickyAPI.KeyValue("Categories", strings.Join(mappings, " | "))) } - return api.Text{}.Add(api.DescriptionList{Items: items}) + return clickyAPI.Text{}.Add(clickyAPI.DescriptionList{Items: items}) } // FilterQuery returns the filters as a single search query string diff --git a/catalog_report/settings_test.go b/report/catalog/settings_test.go similarity index 66% rename from catalog_report/settings_test.go rename to report/catalog/settings_test.go index e1528216e..a300b5dd5 100644 --- a/catalog_report/settings_test.go +++ b/report/catalog/settings_test.go @@ -1,4 +1,4 @@ -package catalog_report +package catalog import ( "os" @@ -20,11 +20,10 @@ thresholds: staleDays: 60 reviewOverdueDays: 30 categoryMappings: - rbac.granted: - - PermissionGranted - - PermissionAdded - backup.failed: - - BackupFailed + - category: rbac.granted + filter: 'changeType == "PermissionGranted" || changeType == "PermissionAdded"' + - category: backup.failed + filter: 'changeType == "BackupFailed"' ` path := filepath.Join(os.TempDir(), "test-settings.yaml") Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) @@ -35,15 +34,31 @@ categoryMappings: Expect(s.Filters).To(Equal([]string{"type!=Kubernetes::ConfigMap", "type!=Kubernetes::Secret"})) Expect(s.Thresholds.StaleDays).To(Equal(60)) Expect(s.Thresholds.ReviewOverdueDays).To(Equal(30)) - Expect(s.CategoryMappings).To(HaveKey("rbac.granted")) - Expect(s.CategoryMappings["rbac.granted"]).To(Equal([]string{"PermissionGranted", "PermissionAdded"})) - Expect(s.CategoryMappings["backup.failed"]).To(Equal([]string{"BackupFailed"})) + Expect(s.CategoryMappings).To(HaveLen(2)) + Expect(s.CategoryMappings[0].Category).To(Equal("rbac.granted")) + Expect(s.CategoryMappings[0].Filter).To(Equal(`changeType == "PermissionGranted" || changeType == "PermissionAdded"`)) + Expect(s.CategoryMappings[1].Category).To(Equal("backup.failed")) + Expect(s.CategoryMappings[1].Filter).To(Equal(`changeType == "BackupFailed"`)) }) ginkgo.It("returns error for missing file", func() { _, err := LoadSettings("/nonexistent/path.yaml") Expect(err).To(HaveOccurred()) }) + + ginkgo.It("rejects the old category mapping shape", func() { + content := ` +categoryMappings: + backup.failed: + - BackupFailed +` + path := filepath.Join(os.TempDir(), "legacy-settings.yaml") + Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) + defer os.Remove(path) + + _, err := LoadSettings(path) + Expect(err).To(HaveOccurred()) + }) }) ginkgo.Describe("LoadDefaultSettings", func() { @@ -53,10 +68,10 @@ categoryMappings: Expect(s.Filters).To(ContainElement("type!=Kubernetes::ConfigMap")) Expect(s.Thresholds.StaleDays).To(Equal(90)) Expect(s.Thresholds.ReviewOverdueDays).To(Equal(90)) - Expect(s.CategoryMappings).To(HaveKey("rbac.granted")) - Expect(s.CategoryMappings["rbac.granted"]).To(ContainElement("PermissionAdded")) - Expect(s.CategoryMappings["backup.failed"]).To(ContainElement("BACKUP_DB@high")) - Expect(s.CategoryMappings["deployment.failed"]).To(ContainElement("CodeDeployment@failed")) + Expect(s.CategoryMappings).ToNot(BeEmpty()) + Expect(s.CategoryMappings[0].Category).To(Equal("rbac.granted")) + Expect(s.CategoryMappings[0].Filter).To(ContainSubstring("PermissionGranted")) + Expect(s.CategoryMappings).To(ContainElement(HaveField("Category", "deployment.failed"))) }) }) @@ -66,7 +81,7 @@ categoryMappings: Expect(err).ToNot(HaveOccurred()) Expect(source).To(Equal(EmbeddedSettingsSource)) Expect(s.Filters).To(ContainElement("type!=Kubernetes::Secret")) - Expect(s.CategoryMappings).To(HaveKey("backup.failed")) + Expect(s.CategoryMappings).To(ContainElement(HaveField("Category", "backup.failed"))) }) ginkgo.It("overlays file settings on top of embedded defaults", func() { @@ -76,10 +91,10 @@ filters: thresholds: staleDays: 60 categoryMappings: - backup.failed: - - BACKUP_DB@high - deployment.failed: - - CodeDeployment@failed + - category: backup.failed + filter: 'changeType == "BACKUP_DB" && severity == "high"' + - category: deployment.failed + filter: 'changeType == "CodeDeployment" && severity == "failed"' ` path := filepath.Join(os.TempDir(), "overlay-settings.yaml") Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) @@ -92,9 +107,11 @@ categoryMappings: Expect(s.Filters).To(Equal([]string{"name=test"})) Expect(s.Thresholds.StaleDays).To(Equal(60)) Expect(s.Thresholds.ReviewOverdueDays).To(Equal(90)) - Expect(s.CategoryMappings["backup.failed"]).To(Equal([]string{"BACKUP_DB@high"})) - Expect(s.CategoryMappings["deployment.failed"]).To(Equal([]string{"CodeDeployment@failed"})) - Expect(s.CategoryMappings).To(HaveKey("rbac.granted")) + Expect(s.CategoryMappings).To(HaveLen(2)) + Expect(s.CategoryMappings[0].Category).To(Equal("backup.failed")) + Expect(s.CategoryMappings[0].Filter).To(Equal(`changeType == "BACKUP_DB" && severity == "high"`)) + Expect(s.CategoryMappings[1].Category).To(Equal("deployment.failed")) + Expect(s.CategoryMappings[1].Filter).To(Equal(`changeType == "CodeDeployment" && severity == "failed"`)) }) }) diff --git a/report/scraper/scraper.go b/report/scraper/scraper.go new file mode 100644 index 000000000..0965c0f38 --- /dev/null +++ b/report/scraper/scraper.go @@ -0,0 +1,104 @@ +package scraper + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "sort" + + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/models" + "github.com/flanksource/duty/query" + "github.com/google/uuid" + + "github.com/flanksource/incident-commander/api" +) + +var knownBackendKeys = map[string]bool{ + "kubernetes": true, + "aws": true, + "azure": true, + "gcp": true, + "file": true, + "sql": true, + "http": true, + "trivy": true, + "terraform": true, + "githubActions": true, + "slack": true, + "kubernetesFile": true, +} + +func BuildScraperInfo(ctx context.Context, scraperID uuid.UUID) (*api.ScraperInfo, error) { + var scraper models.ConfigScraper + if err := ctx.DB().Where("id = ?", scraperID).First(&scraper).Error; err != nil { + return nil, ctx.Oops().Wrapf(err, "scraper %s not found", scraperID) + } + + types := parseSpecTypes(scraper.Spec) + if types == nil { + types = []string{} + } + + info := &api.ScraperInfo{ + ID: scraper.ID.String(), + Name: scraper.Name, + Namespace: scraper.Namespace, + Source: scraper.Source, + CreatedAt: scraper.CreatedAt.Format("2006-01-02T15:04:05Z"), + SpecHash: specSHA256(scraper.Spec), + Types: types, + } + + if scraper.Description != "" { + info.Description = scraper.Description + } + + if scraper.UpdatedAt != nil { + info.UpdatedAt = scraper.UpdatedAt.Format("2006-01-02T15:04:05Z") + } + + if scraper.CreatedBy != nil { + var person models.Person + if err := ctx.DB().Where("id = ?", scraper.CreatedBy).First(&person).Error; err == nil { + info.CreatedBy = person.GetName() + } + } + + source, err := query.GetGitOpsSource(ctx, scraperID) + if err != nil { + ctx.Logger.V(3).Infof("no gitops source for scraper %s: %v", scraperID, err) + } else if source.Git.URL != "" { + info.GitOps = &source + } + + return info, nil +} + +func parseSpecTypes(spec string) []string { + if spec == "" { + return nil + } + + var parsed map[string]any + if err := json.Unmarshal([]byte(spec), &parsed); err != nil { + return nil + } + + var types []string + for key := range parsed { + if knownBackendKeys[key] { + types = append(types, key) + } + } + sort.Strings(types) + return types +} + +func specSHA256(spec string) string { + if spec == "" { + return "" + } + h := sha256.Sum256([]byte(spec)) + return fmt.Sprintf("%x", h) +} diff --git a/report/testdata/kitchen-sink.yaml b/report/testdata/kitchen-sink.yaml index 59724c570..f011ae10a 100644 --- a/report/testdata/kitchen-sink.yaml +++ b/report/testdata/kitchen-sink.yaml @@ -1664,7 +1664,7 @@ catalogReport: audit: buildCommit: "3b3a1a0f" buildVersion: "v1.47.0" - gitStatus: " M catalog_report/report.go\n M db/rbac.go" + gitStatus: " M report/catalog/report.go\n M db/rbac.go" options: title: "Production EKS Cluster Report" since: "720h" diff --git a/views/render_facet.go b/views/render_facet.go index 776182ab0..f7e3d0b85 100644 --- a/views/render_facet.go +++ b/views/render_facet.go @@ -115,7 +115,11 @@ func renderFacetWithData(ctx context.Context, data any, format string, opts *v1. return report.RenderHTTP(ctx, baseURL, token, data, format, viewEntryFile) } - return report.RenderCLI(data, format, viewEntryFile) + result, err := report.RenderCLI(data, format, viewEntryFile) + if err != nil { + return nil, err + } + return result.Data, nil } func resolveFacetConnection(ctx context.Context, opts *v1.FacetOptions) (baseURL, token, timestampURL string, err error) { From ac3831263568c2078561a530a51b8eb5a83040d3 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 10 Apr 2026 15:48:13 +0300 Subject: [PATCH 36/48] feat(api): detect and auto-heal misaligned backend server urls Adds detection for HTML responses from JSON endpoints (indicating frontend at wrong URL), auto-healing by appending /api and retrying. Includes new context add command and requires specific bearer audience for azure login flow. --- cmd/connection_browser.go | 52 ++++++++++++---- cmd/connection_test_cmd.go | 18 ++++++ cmd/context.go | 123 ++++++++++++++++++++++++++++++++++++- sdk/client.go | 39 +++++++++++- 4 files changed, 216 insertions(+), 16 deletions(-) diff --git a/cmd/connection_browser.go b/cmd/connection_browser.go index 50c447e83..b644eabff 100644 --- a/cmd/connection_browser.go +++ b/cmd/connection_browser.go @@ -7,6 +7,7 @@ import ( "fmt" "net/url" "os" + "sort" "strings" "time" @@ -30,15 +31,16 @@ import ( ) type browserLoginFlags struct { - Name string - Namespace string - URL string - Domains []string - WaitForURL string - Timeout time.Duration - Cookies bool - Session bool - Bearer bool + Name string + Namespace string + URL string + Domains []string + WaitForURL string + Timeout time.Duration + Cookies bool + Session bool + Bearer bool + RequireBearerAud string } type browserSessionData struct { @@ -124,6 +126,7 @@ func init() { browserFlags.Session = true browserFlags.Bearer = true } + browserFlags.RequireBearerAud = "graph.microsoft.com" } connectionLoginCmd.AddCommand(connectionLoginAzureCmd) @@ -280,6 +283,7 @@ func waitForLoginComplete(browserCtx gocontext.Context, flags browserLoginFlags) go func() { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() + lastReported := "" for { select { case <-ticker.C: @@ -287,12 +291,34 @@ func waitForLoginComplete(browserCtx gocontext.Context, flags browserLoginFlags) if err != nil { continue } + validAuds := make([]string, 0) + var matched string + var matchedJWT *connection.JWT for aud, token := range extractBearerTokens(session) { jwt := connection.DecodeJWT(token) - if jwt != nil && time.Until(jwt.ExpiresAt) > 0 { - fmt.Fprintf(os.Stderr, "Found valid token for %s (expires in %s)\n", aud, time.Until(jwt.ExpiresAt).Round(time.Second)) - doneCh <- struct{}{} - return + if jwt == nil || time.Until(jwt.ExpiresAt) <= 0 { + continue + } + validAuds = append(validAuds, aud) + if matched != "" { + continue + } + if flags.RequireBearerAud == "" || strings.Contains(aud, flags.RequireBearerAud) { + matched = aud + matchedJWT = jwt + } + } + if matched != "" { + fmt.Fprintf(os.Stderr, "Found valid token for %s (expires in %s)\n", matched, time.Until(matchedJWT.ExpiresAt).Round(time.Second)) + doneCh <- struct{}{} + return + } + if flags.RequireBearerAud != "" && len(validAuds) > 0 { + sort.Strings(validAuds) + summary := strings.Join(validAuds, ", ") + if summary != lastReported { + fmt.Fprintf(os.Stderr, "Waiting for %s token (have: %s)\n", flags.RequireBearerAud, summary) + lastReported = summary } } case <-browserCtx.Done(): diff --git a/cmd/connection_test_cmd.go b/cmd/connection_test_cmd.go index 81d915a6c..ff96f3f4e 100644 --- a/cmd/connection_test_cmd.go +++ b/cmd/connection_test_cmd.go @@ -2,6 +2,7 @@ package cmd import ( gocontext "context" + "errors" "fmt" "os" @@ -100,6 +101,23 @@ func runConnectionTestFromDB(name, namespace string, overrides *connectionFlags) } func runConnectionTestViaAPI(mcCtx *MCContext, name, namespace string) (any, error) { + result, err := callConnectionTestAPI(mcCtx, name, namespace) + if !errors.Is(err, sdk.ErrHTMLResponse) { + return result, err + } + + upgraded, upErr := ensureAPIBase(mcCtx) + if upErr != nil { + return nil, fmt.Errorf("%w (probe failed: %v)", err, upErr) + } + if !upgraded { + return nil, err + } + fmt.Fprintf(os.Stderr, "Upgraded context %q server to %s\n", mcCtx.Name, mcCtx.Server) + return callConnectionTestAPI(mcCtx, name, namespace) +} + +func callConnectionTestAPI(mcCtx *MCContext, name, namespace string) (any, error) { client := sdk.New(mcCtx.Server, mcCtx.Token) conn, err := client.GetConnection(name, namespace) diff --git a/cmd/context.go b/cmd/context.go index cf1472a6d..844b75ffd 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + nethttp "net/http" "net/url" "os" "path/filepath" @@ -217,8 +218,128 @@ var contextCurrentCmd = &cobra.Command{ }, } +var ( + contextAddName string + contextAddServer string + contextAddDB string + contextAddToken string + contextAddUse bool +) + +var contextAddCmd = &cobra.Command{ + Use: "add", + Short: "Add or update a Mission Control context", + Long: `Add a new context (or update an existing one by name). At least one of --server +or --db-url is required. Pass --use to switch to the new context immediately. + +Examples: + mission-control context add --name local --db-url "$DB_URL" + mission-control context add --name beta --server https://beta.flanksource.com --token "$TOKEN" --use`, + RunE: func(cmd *cobra.Command, args []string) error { + if contextAddName == "" { + return fmt.Errorf("--name is required") + } + if contextAddServer == "" && contextAddDB == "" { + return fmt.Errorf("at least one of --server or --db-url is required") + } + + cfg, err := LoadConfig() + if err != nil { + return err + } + + ctx := MCContext{ + Name: contextAddName, + Server: strings.TrimRight(contextAddServer, "/"), + DB: contextAddDB, + Token: contextAddToken, + } + existing := cfg.GetContext(contextAddName) != nil + cfg.SetContext(ctx) + + if contextAddUse || cfg.CurrentContext == "" { + cfg.CurrentContext = contextAddName + } + + if err := SaveConfig(cfg); err != nil { + return err + } + + action := "Added" + if existing { + action = "Updated" + } + fmt.Fprintf(cmd.OutOrStdout(), "%s context %q\n", action, contextAddName) + if cfg.CurrentContext == contextAddName { + fmt.Fprintf(cmd.OutOrStdout(), "Switched to context %q\n", contextAddName) + } + return nil + }, +} + +// ensureAPIBase probes serverURL + "/api/db/connections" and, if that path +// returns JSON, appends "/api" to the stored server URL and saves the config. +// Returns true when the context was upgraded. Used to self-heal after the SDK +// reports ErrHTMLResponse. +func ensureAPIBase(ctx *MCContext) (bool, error) { + if ctx == nil || ctx.Server == "" { + return false, nil + } + if strings.HasSuffix(strings.TrimRight(ctx.Server, "/"), "/api") { + return false, nil + } + + probeURL := strings.TrimRight(ctx.Server, "/") + "/api/db/connections?limit=0" + req, err := nethttp.NewRequest(nethttp.MethodGet, probeURL, nil) + if err != nil { + return false, err + } + if ctx.Token != "" { + req.Header.Set("Authorization", "Bearer "+ctx.Token) + } + req.Header.Set("Accept", "application/json") + + resp, err := nethttp.DefaultClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + buf := make([]byte, 512) + n, _ := resp.Body.Read(buf) + body := strings.TrimLeft(string(buf[:n]), " \t\r\n") + ct := strings.ToLower(resp.Header.Get("Content-Type")) + if strings.Contains(ct, "text/html") || strings.HasPrefix(body, "<") { + return false, nil + } + if !strings.Contains(ct, "json") && !strings.HasPrefix(body, "[") && !strings.HasPrefix(body, "{") { + return false, nil + } + + cfg, err := LoadConfig() + if err != nil { + return false, err + } + stored := cfg.GetContext(ctx.Name) + if stored == nil { + return false, nil + } + stored.Server = strings.TrimRight(stored.Server, "/") + "/api" + ctx.Server = stored.Server + if err := SaveConfig(cfg); err != nil { + return false, err + } + return true, nil +} + func init() { - ContextCmd.AddCommand(contextUseCmd, contextListCmd, contextCurrentCmd) + contextAddCmd.Flags().StringVar(&contextAddName, "name", "", "Context name (required)") + contextAddCmd.Flags().StringVar(&contextAddServer, "server", "", "Mission Control server URL") + contextAddCmd.Flags().StringVar(&contextAddDB, "db-url", "", "Direct database connection URL") + contextAddCmd.Flags().StringVar(&contextAddToken, "token", "", "API token for the server") + contextAddCmd.Flags().BoolVar(&contextAddUse, "use", false, "Switch to this context after adding") + + ContextCmd.AddCommand(contextUseCmd, contextListCmd, contextCurrentCmd, contextAddCmd) Root.AddCommand(ContextCmd) Root.PersistentFlags().StringVar(&contextFlag, "context", "", "Mission Control context to use") } diff --git a/sdk/client.go b/sdk/client.go index 661ac0988..5b2abf292 100644 --- a/sdk/client.go +++ b/sdk/client.go @@ -2,13 +2,21 @@ package sdk import ( "context" + "encoding/json" + "errors" "fmt" "net/url" + "strings" "github.com/flanksource/commons/http" "github.com/flanksource/duty/models" ) +// ErrHTMLResponse is returned when the server responded with HTML on a JSON +// endpoint — typically because the configured server URL points at the +// user-facing frontend rather than the /api backend. +var ErrHTMLResponse = errors.New("server returned HTML instead of JSON (is the backend at /api?)") + type Client struct { *http.Client } @@ -18,11 +26,35 @@ func New(serverURL, token string) *Client { Client: http.NewClient(). BaseURL(serverURL). Header("Authorization", "Bearer "+token). + Header("Accept", "application/json"). Header("Content-Type", "application/json"). UserAgent("mission-control-cli"), } } +// decodeJSON parses a response body as JSON, returning ErrHTMLResponse if the +// body looks like HTML (frontend served instead of backend JSON). +func decodeJSON(r *http.Response, out any) error { + body, err := r.AsString() + if err != nil { + return err + } + if looksLikeHTML(r.Header.Get("Content-Type"), body) { + return ErrHTMLResponse + } + if err := json.Unmarshal([]byte(body), out); err != nil { + return fmt.Errorf("failed to decode JSON response: %w", err) + } + return nil +} + +func looksLikeHTML(contentType, body string) bool { + if strings.Contains(strings.ToLower(contentType), "text/html") { + return true + } + return strings.HasPrefix(strings.TrimLeft(body, " \t\r\n"), "<") +} + func (c *Client) GetConnection(name, namespace string) (*models.Connection, error) { var connections []models.Connection r, err := c.R(context.Background()). @@ -37,7 +69,7 @@ func (c *Client) GetConnection(name, namespace string) (*models.Connection, erro if !r.IsOK() { return nil, fmt.Errorf("server returned %d", r.StatusCode) } - if err := r.Into(&connections); err != nil { + if err := decodeJSON(r, &connections); err != nil { return nil, err } if len(connections) == 0 { @@ -74,9 +106,12 @@ func (c *Client) TestConnection(id string) (*TestResult, error) { } if !r.IsOK() { body, _ := r.AsString() + if looksLikeHTML(r.Header.Get("Content-Type"), body) { + return nil, ErrHTMLResponse + } return nil, fmt.Errorf("test failed (%d): %s", r.StatusCode, body) } - if err := r.Into(&result); err != nil { + if err := decodeJSON(r, &result); err != nil { return &result, err } return &result, nil From 94e0ed981be915d7e6d71c8e6bb27433e2f52aa4 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 12 Apr 2026 15:02:38 +0300 Subject: [PATCH 37/48] feat(api): add bearer token scope filtering for azure portal login Support filtering bearer tokens by scope in addition to audience. Automatically selects account in azure portal, displays token counts, and validates tokens match both audience and scope requirements before saving connections. --- cmd/connection_browser.go | 183 +++++++++++++++++++++++++++++++------- connection/jwt.go | 7 ++ 2 files changed, 157 insertions(+), 33 deletions(-) diff --git a/cmd/connection_browser.go b/cmd/connection_browser.go index b644eabff..6591db2a0 100644 --- a/cmd/connection_browser.go +++ b/cmd/connection_browser.go @@ -19,6 +19,8 @@ import ( "github.com/chromedp/cdproto/storage" "github.com/chromedp/chromedp" "github.com/flanksource/clicky" + "github.com/flanksource/clicky/api" + "github.com/flanksource/clicky/api/icons" "github.com/flanksource/duty" "github.com/flanksource/duty/models" "github.com/flanksource/duty/shutdown" @@ -31,16 +33,17 @@ import ( ) type browserLoginFlags struct { - Name string - Namespace string - URL string - Domains []string - WaitForURL string - Timeout time.Duration - Cookies bool - Session bool - Bearer bool - RequireBearerAud string + Name string + Namespace string + URL string + Domains []string + WaitForURL string + Timeout time.Duration + Cookies bool + Session bool + Bearer bool + RequireBearerAud string + RequireBearerScope string } type browserSessionData struct { @@ -64,6 +67,8 @@ Examples: var browserFlags browserLoginFlags var azureLoginURL string +var azurePageURL string +var azureRequiredScope string var ( browserTestName string @@ -115,9 +120,11 @@ func init() { connectionLoginCmd.RunE = runBrowserLogin connectionLoginAzureCmd.PersistentFlags().StringVar(&azureLoginURL, "login-url", "https://portal.azure.com", "URL to open for browser login") + connectionLoginAzureCmd.PersistentFlags().StringVar(&azurePageURL, "page", "https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Overview", "Portal page to navigate to after login") + connectionLoginAzureCmd.PersistentFlags().StringVar(&azureRequiredScope, "required-scope", "AuditLog.Read.All", "Required scope substring in the captured msgraph token") connectionLoginAzureCmd.PreRun = func(cmd *cobra.Command, args []string) { - browserFlags.URL = azureLoginURL + browserFlags.URL = azurePageURL if len(browserFlags.Domains) == 0 { browserFlags.Domains = []string{".azure.com", ".microsoft.com", ".microsoftonline.com", ".windows.net", ".live.com"} } @@ -127,6 +134,7 @@ func init() { browserFlags.Bearer = true } browserFlags.RequireBearerAud = "graph.microsoft.com" + browserFlags.RequireBearerScope = azureRequiredScope } connectionLoginCmd.AddCommand(connectionLoginAzureCmd) @@ -163,6 +171,12 @@ func runBrowserLogin(cmd *cobra.Command, args []string) error { return err } + if browserFlags.RequireBearerAud != "" || browserFlags.RequireBearerScope != "" { + if !hasRequiredToken(data.BearerTokens, browserFlags) { + return fmt.Errorf("no valid token found matching audience=%q scope=%q", browserFlags.RequireBearerAud, browserFlags.RequireBearerScope) + } + } + return saveConnection(cmd, browserFlags, data) } @@ -192,6 +206,7 @@ func launchBrowserAndCapture(ctx gocontext.Context, flags browserLoginFlags) (*b fmt.Fprintln(os.Stderr, "Please log in. Press Enter when done.") } + autoSelectAccountPicker(browserCtx) waitForLoginComplete(browserCtx, flags) data := &browserSessionData{} @@ -234,10 +249,14 @@ func launchBrowserAndCapture(ctx gocontext.Context, flags browserLoginFlags) (*b } else if verbose >= 1 { fmt.Fprintln(os.Stderr, state.Pretty().ANSI()) } else { - // Default: just show bearer tokens - for _, token := range data.BearerTokens { - if jwt := connection.DecodeJWT(token); jwt != nil { - fmt.Fprintln(os.Stderr, jwt.Pretty().ANSI()) + selectedAud, _ := selectBearerToken(data.BearerTokens, flags.RequireBearerAud) + for _, aud := range sortedAudiences(data.BearerTokens) { + if jwt := connection.DecodeJWT(data.BearerTokens[aud]); jwt != nil { + t := jwt.Pretty() + if aud == selectedAud { + t = api.Text{}.Add(icons.Check.WithStyle("text-green-500")).Append(" bearer ").Add(t) + } + fmt.Fprintln(os.Stderr, t.ANSI()) } } } @@ -245,6 +264,30 @@ func launchBrowserAndCapture(ctx gocontext.Context, flags browserLoginFlags) (*b return data, nil } +func autoSelectAccountPicker(browserCtx gocontext.Context) { + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + var nodes int + err := chromedp.Run(browserCtx, chromedp.Evaluate( + `document.querySelectorAll('#tilesHolder .tile-container .table[role="button"]').length`, &nodes)) + if err != nil || nodes == 0 { + continue + } + fmt.Fprintf(os.Stderr, "Account picker detected, selecting first account\n") + _ = chromedp.Run(browserCtx, chromedp.Click( + `#tilesHolder .tile-container:first-child .table[role="button"]`, chromedp.ByQuery)) + return + case <-browserCtx.Done(): + return + } + } + }() +} + func waitForLoginComplete(browserCtx gocontext.Context, flags browserLoginFlags) { doneCh := make(chan struct{}, 3) @@ -303,21 +346,27 @@ func waitForLoginComplete(browserCtx gocontext.Context, flags browserLoginFlags) if matched != "" { continue } - if flags.RequireBearerAud == "" || strings.Contains(aud, flags.RequireBearerAud) { + audMatches := flags.RequireBearerAud == "" || strings.Contains(aud, flags.RequireBearerAud) + scopeMatches := flags.RequireBearerScope == "" || strings.Contains(jwt.Scopes, flags.RequireBearerScope) + if audMatches && scopeMatches { matched = aud matchedJWT = jwt } } if matched != "" { - fmt.Fprintf(os.Stderr, "Found valid token for %s (expires in %s)\n", matched, time.Until(matchedJWT.ExpiresAt).Round(time.Second)) + fmt.Fprintf(os.Stderr, "Found valid token for %s (scopes=%d, expires in %s)\n", matched, matchedJWT.ScopeCount(), time.Until(matchedJWT.ExpiresAt).Round(time.Second)) doneCh <- struct{}{} return } - if flags.RequireBearerAud != "" && len(validAuds) > 0 { + if (flags.RequireBearerAud != "" || flags.RequireBearerScope != "") && len(validAuds) > 0 { sort.Strings(validAuds) summary := strings.Join(validAuds, ", ") if summary != lastReported { - fmt.Fprintf(os.Stderr, "Waiting for %s token (have: %s)\n", flags.RequireBearerAud, summary) + waiting := flags.RequireBearerAud + if flags.RequireBearerScope != "" { + waiting += " with scope " + flags.RequireBearerScope + } + fmt.Fprintf(os.Stderr, "Waiting for %s token (have: %s)\n", waiting, summary) lastReported = summary } } @@ -375,8 +424,26 @@ func extractSessionStorage(browserCtx gocontext.Context) (map[string]string, err return result, nil } +func hasRequiredToken(tokens map[string]string, flags browserLoginFlags) bool { + for aud, token := range tokens { + if flags.RequireBearerAud != "" && !strings.Contains(aud, flags.RequireBearerAud) { + continue + } + jwt := connection.DecodeJWT(token) + if jwt == nil || time.Until(jwt.ExpiresAt) <= 0 { + continue + } + if flags.RequireBearerScope != "" && !strings.Contains(jwt.Scopes, flags.RequireBearerScope) { + continue + } + return true + } + return false +} + func extractBearerTokens(session map[string]string) map[string]string { tokens := make(map[string]string) + scopeCounts := make(map[string]int) for key, value := range session { if !strings.Contains(key, "accesstoken") { continue @@ -393,11 +460,41 @@ func extractBearerTokens(session map[string]string) map[string]string { if jwt == nil || jwt.Audience == "" { continue } - tokens[jwt.Audience] = secret + if jwt.ScopeCount() > scopeCounts[jwt.Audience] { + tokens[jwt.Audience] = secret + scopeCounts[jwt.Audience] = jwt.ScopeCount() + } } return tokens } +func selectBearerToken(tokens map[string]string, requiredAud string) (string, error) { + var bestAud string + var bestScopes int + for aud, token := range tokens { + if !strings.Contains(aud, requiredAud) { + continue + } + if jwt := connection.DecodeJWT(token); jwt != nil && jwt.ScopeCount() > bestScopes { + bestAud = aud + bestScopes = jwt.ScopeCount() + } + } + if bestAud != "" { + return bestAud, nil + } + return "", fmt.Errorf("no token found for required audience %q (have: %s)", requiredAud, strings.Join(sortedAudiences(tokens), ", ")) +} + +func sortedAudiences(tokens map[string]string) []string { + auds := make([]string, 0, len(tokens)) + for aud := range tokens { + auds = append(auds, aud) + } + sort.Strings(auds) + return auds +} + func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSessionData) error { ctx, stop, err := duty.Start("mission-control", duty.ClientOnly) if err != nil { @@ -470,23 +567,16 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe for aud, token := range data.BearerTokens { props["bearer_"+aud] = token } - for aud, token := range data.BearerTokens { - if strings.Contains(aud, "graph.microsoft.com") { - props["bearer"] = token - break - } - props["bearer"] = token + selectedAud, err := selectBearerToken(data.BearerTokens, flags.RequireBearerAud) + if err != nil { + return err } + props["bearer"] = data.BearerTokens[selectedAud] } connURL := flags.URL - if props["bearer"] != "" { - for aud := range data.BearerTokens { - if strings.Contains(aud, "graph.microsoft.com") { - connURL = "https://graph.microsoft.com/v1.0/me" - break - } - } + if props["bearer"] != "" && strings.Contains(flags.RequireBearerAud, "graph.microsoft.com") { + connURL = "https://graph.microsoft.com/v1.0/me" } conn := models.Connection{ @@ -518,6 +608,33 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe action = "updated" } fmt.Fprintf(cmd.OutOrStdout(), "Connection '%s' %s in namespace '%s'\n", flags.Name, action, flags.Namespace) + + if len(data.Cookies) > 0 { + fmt.Fprintf(cmd.OutOrStdout(), " Cookies: %d\n", len(data.Cookies)) + } + if len(data.SessionStorage) > 0 { + fmt.Fprintf(cmd.OutOrStdout(), " Session storage: %d keys\n", len(data.SessionStorage)) + } + if len(data.BearerTokens) > 0 { + selectedAud, _ := selectBearerToken(data.BearerTokens, flags.RequireBearerAud) + for _, aud := range sortedAudiences(data.BearerTokens) { + jwt := connection.DecodeJWT(data.BearerTokens[aud]) + if jwt == nil { + continue + } + t := api.Text{} + if aud == selectedAud { + t = t.Add(icons.Check.WithStyle("text-green-500")).Append(" bearer", "font-bold") + } else { + t = t.Append(" bearer_"+aud, "text-muted") + } + t = t.Appendf(" aud=%s", jwt.Audience). + Appendf(" scopes=%d", jwt.ScopeCount()). + Appendf(" expires=%s", time.Until(jwt.ExpiresAt).Round(time.Second)) + fmt.Fprintln(cmd.OutOrStdout(), t.ANSI()) + } + } + return nil } diff --git a/connection/jwt.go b/connection/jwt.go index adec57995..a8473667e 100644 --- a/connection/jwt.go +++ b/connection/jwt.go @@ -53,6 +53,13 @@ func (j JWT) Pretty() api.Text { return t } +func (j JWT) ScopeCount() int { + if j.Scopes == "" { + return 0 + } + return len(strings.Fields(j.Scopes)) +} + func (j JWT) PrettyFull() api.Text { t := j.Pretty() if j.Raw != "" { From cbf79aeb9a058a2b5e0acb2786407e6ae8a36727 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 14 Apr 2026 11:25:51 +0300 Subject: [PATCH 38/48] chore(build): simplify makefile tool paths and regenerate kitchen-sink test data Removes $(LOCALBIN)/ prefix from deps tool invocations since deps is now directly in PATH. Adds build target for kitchen-sink.json compiled from schema examples and test fixtures. Updates gitignore with additional test artifacts and generated files. --- .gitignore | 2 +- Makefile | 9 +- cmd/application.go | 2 +- cmd/rbac.go | 8 +- db/rbac.go | 115 +- report/build-kitchen-sink.ts | 165 + report/components/ConfigChangesExamples.tsx | 22 +- report/components/ConfigChangesSection.tsx | 8 +- report/components/change-section-utils.ts | 326 +- report/kitchen-sink-data.ts | 5 +- report/kitchen-sink.json | 3806 +++++++++++++++++++ report/kitchen-sink/ChangesPage.tsx | 18 +- sdk/client_test.go | 73 + 13 files changed, 4497 insertions(+), 62 deletions(-) create mode 100644 report/build-kitchen-sink.ts create mode 100644 report/kitchen-sink.json create mode 100644 sdk/client_test.go diff --git a/.gitignore b/.gitignore index 51ac21daa..bb6fef39a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.bin/ +7.bin/ .kube .vscode/ .idea/ diff --git a/Makefile b/Makefile index 61c5b5992..622e905b4 100644 --- a/Makefile +++ b/Makefile @@ -195,15 +195,15 @@ ginkgo: .PHONY: controller-gen controller-gen: install-deps $(LOCALBIN) - $(LOCALBIN)/deps install controller-gen@$(CONTROLLER_TOOLS_VERSION) --bin-dir $(LOCALBIN) + deps install controller-gen@$(CONTROLLER_TOOLS_VERSION) --bin-dir $(LOCALBIN) .PHONY: golangci-lint golangci-lint: install-deps $(LOCALBIN) - $(LOCALBIN)/deps install golangci/golangci-lint@v$(GOLANGCI_LINT_VERSION) --bin-dir $(LOCALBIN) + deps install golangci/golangci-lint@v$(GOLANGCI_LINT_VERSION) --bin-dir $(LOCALBIN) .PHONY: kustomize kustomize: install-deps $(LOCALBIN) - $(LOCALBIN)/deps install kubernetes-sigs/kustomize@$(KUSTOMIZE_VERSION) --bin-dir $(LOCALBIN) + deps install kubernetes-sigs/kustomize@$(KUSTOMIZE_VERSION) --bin-dir $(LOCALBIN) .PHONY: docs\:mcp docs\:mcp: ## Generate MCP tools reference documentation @@ -211,6 +211,9 @@ docs\:mcp: ## Generate MCP tools reference documentation go run ./hack/gen-mcp-docs > docs/mcp-tools.md @echo "Generated docs/mcp-tools.md" +report/kitchen-sink.json: report/build-kitchen-sink.ts report/testdata/kitchen-sink.yaml + cd report && ./node_modules/.bin/tsx build-kitchen-sink.ts + .PHONY: lint lint: golangci-lint $(GOLANGCI_LINT) run ./... diff --git a/cmd/application.go b/cmd/application.go index e7d59bb6f..f7bd3cad9 100644 --- a/cmd/application.go +++ b/cmd/application.go @@ -91,6 +91,6 @@ var ExportApplication = &cobra.Command{ func init() { ExportApplication.Flags().StringVarP(&exportFormat, "format", "f", "json", "Output format: json, html, pdf, facet-html, facet-pdf") ExportApplication.Flags().StringVarP(&exportOutfile, "out-file", "o", "", "Write output to file instead of stdout") - ExportApplication.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory with TSX report files (overrides embedded reports)") + ExportApplication.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory or TSX file for report rendering (overrides embedded reports)") ApplicationCmd.AddCommand(ExportApplication) } diff --git a/cmd/rbac.go b/cmd/rbac.go index 4d6ce6473..5ec6a64f8 100644 --- a/cmd/rbac.go +++ b/cmd/rbac.go @@ -30,7 +30,7 @@ var ( rbacReviewDays int rbacSince string rbacTitle string - rbacByUser bool + rbacView string ) var ExportRBAC = &cobra.Command{ @@ -99,7 +99,7 @@ func buildRBACOptions(args []string) rbac_report.Options { Title: rbacTitle, StaleDays: rbacStaleDays, ReviewOverdueDays: rbacReviewDays, - ByUser: rbacByUser, + View: rbacView, } if rbacSince != "" { @@ -165,7 +165,7 @@ func init() { ExportRBAC.Flags().IntVar(&rbacReviewDays, "review-days", 90, "Days without review before access is flagged overdue") ExportRBAC.Flags().StringVar(&rbacSince, "since", "2160h", "Changelog time range (Go duration, default 90 days)") ExportRBAC.Flags().StringVar(&rbacTitle, "title", "", "Report title (default auto-generated)") - ExportRBAC.Flags().BoolVar(&rbacByUser, "by-user", false, "Group report by user instead of resource") - ExportRBAC.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory with TSX report files (overrides embedded reports)") + ExportRBAC.Flags().StringVar(&rbacView, "view", "resource", "Report view: resource, user, or matrix") + ExportRBAC.Flags().StringVar(&report.SourceDir, "report-source", "", "Local directory or TSX file for report rendering (overrides embedded reports)") RBACCmd.AddCommand(ExportRBAC) } diff --git a/db/rbac.go b/db/rbac.go index f82d259fb..756c28821 100644 --- a/db/rbac.go +++ b/db/rbac.go @@ -8,7 +8,6 @@ import ( dutyQuery "github.com/flanksource/duty/query" "github.com/flanksource/duty/types" "github.com/google/uuid" - "github.com/samber/lo" "github.com/flanksource/incident-commander/api" ) @@ -28,6 +27,10 @@ type RBACAccessRow struct { LastReviewedAt *time.Time `gorm:"column:last_reviewed_at"` } +func (r RBACAccessRow) QueryLogSummary() string { + return r.ConfigType +} + func (r RBACAccessRow) RoleSource() string { if r.GroupName != nil && *r.GroupName != "" { return fmt.Sprintf("group:%s", *r.GroupName) @@ -35,7 +38,14 @@ func (r RBACAccessRow) RoleSource() string { return "direct" } -func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector, recursive bool) ([]RBACAccessRow, error) { +func GetRBACAccessByConfigIDs(ctx context.Context, configIDs []uuid.UUID) ([]RBACAccessRow, error) { + return GetRBACAccess(ctx, nil, false, configIDs...) +} + +func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector, recursive bool, configIDs ...uuid.UUID) (results []RBACAccessRow, err error) { + timer := dutyQuery.NewQueryLogger(ctx).Start("RBACAccess").Arg("configIDs", len(configIDs)).Arg("selectors", len(selectors)) + defer timer.End(&err) + q := ctx.DB(). Table("config_access_summary"). Select(`config_access_summary.config_id, @@ -53,50 +63,107 @@ func GetRBACAccess(ctx context.Context, selectors []types.ResourceSelector, recu Joins("LEFT JOIN external_groups ON config_access_summary.external_group_id = external_groups.id") if len(selectors) > 0 { - configIDs, err := dutyQuery.FindConfigIDsByResourceSelector(ctx, 0, selectors...) + resolved, err := dutyQuery.FindConfigIDsByResourceSelector(ctx, 0, selectors...) if err != nil { return nil, ctx.Oops().Wrapf(err, "failed to resolve config selectors") } - if len(configIDs) == 0 { + if len(resolved) == 0 { return nil, nil } if recursive { - configIDs, err = expandConfigChildren(ctx, configIDs) + resolved, err = ExpandConfigChildren(ctx, resolved) if err != nil { return nil, ctx.Oops().Wrapf(err, "failed to expand children") } } + configIDs = append(configIDs, resolved...) + } + + if len(configIDs) > 0 { q = q.Where("config_access_summary.config_id IN (?)", configIDs) } - var rows []RBACAccessRow - if err := q. + if err = q. Order("config_access_summary.config_name, config_access_summary.\"user\""). - Find(&rows).Error; err != nil { + Find(&results).Error; err != nil { return nil, ctx.Oops().Wrapf(err, "failed to query RBAC access") } + timer.Results(results) + return results, nil +} - return rows, nil +func ExpandConfigChildren(ctx context.Context, ids []uuid.UUID) ([]uuid.UUID, error) { + return dutyQuery.ExpandConfigChildren(ctx, ids) } -func expandConfigChildren(ctx context.Context, ids []uuid.UUID) ([]uuid.UUID, error) { - allIDs := make(map[uuid.UUID]struct{}, len(ids)) - for _, id := range ids { - allIDs[id] = struct{}{} - } +// GroupMemberRow represents one (group, user) pair — the membership of an +// external user in an external group. Used by the catalog report audit page +// to enumerate who is in each group that grants access to reported configs. +type GroupMemberRow struct { + GroupID uuid.UUID `gorm:"column:external_group_id"` + GroupName string `gorm:"column:group_name"` + GroupType string `gorm:"column:group_type"` + UserID uuid.UUID `gorm:"column:external_user_id"` + UserName string `gorm:"column:user_name"` + Email string `gorm:"column:email"` + UserType string `gorm:"column:user_type"` + LastSignedInAt *time.Time `gorm:"column:last_signed_in_at"` + MembershipAddedAt time.Time `gorm:"column:membership_created_at"` + MembershipDeletedAt *time.Time `gorm:"column:membership_deleted_at"` +} - for _, id := range ids { - var children []uuid.UUID - if err := ctx.DB().Raw("SELECT child_id FROM lookup_config_children(?, -1)", id.String()). - Scan(&children).Error; err != nil { - return nil, err - } - for _, child := range children { - allIDs[child] = struct{}{} - } +func (r GroupMemberRow) QueryLogSummary() string { + return r.GroupName +} + +// GetGroupMembersForConfigs returns the members of every external group that +// is referenced by an active config_access row on any of the given configs. +// Both active and soft-deleted group memberships are returned so that audit +// reviewers can see users who were recently removed from a group. +func GetGroupMembersForConfigs(ctx context.Context, configIDs []uuid.UUID) (results []GroupMemberRow, err error) { + timer := dutyQuery.NewQueryLogger(ctx).Start("GroupMembers").Arg("configIDs", len(configIDs)) + defer timer.End(&err) + + if len(configIDs) == 0 { + return nil, nil } - return lo.Keys(allIDs), nil + sql := ` + SELECT + eg.id AS external_group_id, + eg.name AS group_name, + eg.group_type AS group_type, + eu.id AS external_user_id, + eu.name AS user_name, + COALESCE(eu.email, '') AS email, + eu.user_type AS user_type, + last_sign_in.last_signed_in_at AS last_signed_in_at, + eug.created_at AS membership_created_at, + eug.deleted_at AS membership_deleted_at + FROM external_user_groups eug + JOIN external_groups eg ON eug.external_group_id = eg.id + JOIN external_users eu ON eug.external_user_id = eu.id + LEFT JOIN ( + SELECT external_user_id, MAX(created_at) AS last_signed_in_at + FROM config_access_logs + GROUP BY external_user_id + ) last_sign_in ON last_sign_in.external_user_id = eu.id + WHERE eug.external_group_id IN ( + SELECT DISTINCT external_group_id + FROM config_access + WHERE config_id IN (?) + AND external_group_id IS NOT NULL + AND deleted_at IS NULL + ) + ORDER BY eg.name ASC, + (eug.deleted_at IS NOT NULL) ASC, + eu.name ASC` + + if err = ctx.DB().Raw(sql, configIDs).Scan(&results).Error; err != nil { + return nil, ctx.Oops().Wrapf(err, "failed to query group members for configs") + } + timer.Results(results) + return results, nil } func GetRBACChangelog(ctx context.Context, configIDs []uuid.UUID, since time.Time) ([]api.RBACChangeEntry, error) { diff --git a/report/build-kitchen-sink.ts b/report/build-kitchen-sink.ts new file mode 100644 index 000000000..42c08afa6 --- /dev/null +++ b/report/build-kitchen-sink.ts @@ -0,0 +1,165 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import yaml from 'js-yaml'; +import type { ConfigChange, ConfigSeverity } from './config-types.ts'; +import type { KitchenSinkData } from './kitchen-sink/KitchenSinkTypes.ts'; + +type SchemaExample = Record & { kind: string }; + +interface SchemaDefinition { + examples?: unknown[]; +} + +interface SchemaDocument { + $defs: Record; +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const baseDataPath = resolve(__dirname, 'testdata/kitchen-sink.yaml'); +// Schema examples are sourced from duty's generated change-types schema at compile time. +const schemaPath = resolve(__dirname, '../../duty/schema/openapi/change-types.schema.json'); +const outputPath = resolve(__dirname, 'kitchen-sink.json'); + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isSchemaExample(value: unknown): value is SchemaExample { + return isRecord(value) && typeof value.kind === 'string'; +} + +function asText(value: unknown): string | undefined { + if (typeof value === 'string') { + const trimmed = value.trim(); + return trimmed || undefined; + } + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + return undefined; +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function kindBase(kind: string): string { + return kind.split('/')[0] ?? kind; +} + +function statusText(example: SchemaExample): string { + return asText(example.status)?.toLowerCase() ?? ''; +} + +function changeTypeForExample(example: SchemaExample): string { + const kind = example.kind; + const status = statusText(example); + + switch (kind) { + case 'Approval/v1': + if (status.includes('approved')) return 'Approved'; + if (status.includes('rejected')) return 'Rejected'; + return 'Approval'; + case 'Backup/v1': + if (status.includes('fail') || status.includes('error')) return 'BackupFailed'; + if (status.includes('running') || status.includes('pending') || status.includes('started') || status.includes('progress')) { + return 'BackupStarted'; + } + if (status.includes('complete') || status.includes('success')) return 'BackupCompleted'; + return 'Backup'; + case 'Restore/v1': + if (status.includes('complete') || status.includes('success')) return 'RestoreCompleted'; + return 'Restore'; + case 'Scale/v1': + return 'Scaling'; + case 'ConfigChange/v1': + return 'diff'; + default: + return kindBase(kind); + } +} + +function severityForExample(example: SchemaExample): ConfigSeverity { + const kind = example.kind; + const status = statusText(example); + + if (kind === 'Backup/v1' || kind === 'Restore/v1' || kind === 'Test/v1') { + if (status.includes('fail') || status.includes('error')) return 'high'; + if (status.includes('pending') || status.includes('running') || status.includes('started')) return 'low'; + } + + if (kind === 'Approval/v1') { + if (status.includes('rejected')) return 'medium'; + if (status.includes('pending')) return 'low'; + } + + if (kind === 'Scale/v1' || kind === 'PermissionChange/v1' || kind === 'UserChange/v1') { + return 'low'; + } + + return 'info'; +} + +function extractStandaloneExamples(schema: SchemaDocument): SchemaExample[] { + const standalone: SchemaExample[] = []; + const defs = schema.$defs ?? {}; + + const rootExamples = defs.ConfigChangeDetailsSchema?.examples ?? []; + for (const example of rootExamples) { + if (isSchemaExample(example)) { + standalone.push(clone(example)); + } + } + + for (const [name, definition] of Object.entries(defs)) { + if (name === 'ConfigChangeDetailsSchema') { + continue; + } + + for (const example of definition.examples ?? []) { + if (isSchemaExample(example)) { + standalone.push(clone(example)); + } + } + } + + return standalone; +} + +function buildSchemaExampleChanges(schema: SchemaDocument): ConfigChange[] { + const examples = extractStandaloneExamples(schema); + const startTimestamp = Date.parse('2026-04-10T23:59:00Z'); + + return examples.map((typedChange, index) => ({ + id: `schema-example-${String(index + 1).padStart(3, '0')}`, + configID: 'schema-example-catalog', + configName: 'Schema Example Catalog', + configType: 'Schema::Example', + changeType: changeTypeForExample(typedChange), + severity: severityForExample(typedChange), + source: 'schema-examples', + createdBy: 'schema-generator', + createdAt: new Date(startTimestamp - (index * 60_000)).toISOString(), + count: 1, + typedChange, + })); +} + +function compileKitchenSink(): KitchenSinkData { + const baseData = yaml.load(readFileSync(baseDataPath, 'utf-8')) as KitchenSinkData; + const schema = JSON.parse(readFileSync(schemaPath, 'utf-8')) as SchemaDocument; + const schemaExampleChanges = buildSchemaExampleChanges(schema); + + return { + ...baseData, + changes: [...(baseData.changes ?? []), ...schemaExampleChanges], + }; +} + +const compiled = compileKitchenSink(); +writeFileSync(outputPath, JSON.stringify(compiled, null, 2) + '\n'); + +if (process.argv.includes('--stdout')) { + process.stdout.write(JSON.stringify(compiled, null, 2)); +} diff --git a/report/components/ConfigChangesExamples.tsx b/report/components/ConfigChangesExamples.tsx index 95158d0dd..8c85b8c98 100644 --- a/report/components/ConfigChangesExamples.tsx +++ b/report/components/ConfigChangesExamples.tsx @@ -12,22 +12,32 @@ function pickMatching(changes: ConfigChange[], predicate: (change: ConfigChange) } export default function ConfigChangesExamples({ changes }: Props) { - if (!changes?.length) { + const available = (changes ?? []).filter((change) => change.source !== 'schema-examples'); + if (!available.length) { return null; } const singleLine = pickMatching( - changes, + available, (change) => !change.summary || change.summary.length <= 72 || Boolean(change.typedChange?.kind), 6, ); const typedDiffs = pickMatching( - changes, - (change) => ['Deployment/v1', 'Promotion/v1', 'Rollback/v1', 'Scaling/v1', 'CostChange/v1'].includes(change.typedChange?.kind ?? ''), + available, + (change) => [ + 'ConfigChange/v1', + 'Promotion/v1', + 'Scale/v1', + 'Restore/v1', + 'Deployment/v1', + 'Rollback/v1', + 'Scaling/v1', + 'CostChange/v1', + ].includes(change.typedChange?.kind ?? ''), 5, ); const visualStates = pickMatching( - changes, + available, (change) => ( (change.severity && change.severity !== 'info') || Boolean(change.artifacts?.length) @@ -52,7 +62,7 @@ export default function ConfigChangesExamples({ changes }: Props) { {typedDiffs.length > 0 && (
- Typed changes show richer before/after chips for images, environments, versions, replicas, and costs instead of a generic diff label. + Typed changes show richer before/after chips for nested config diffs, promotions, restores, replicas, and legacy deployment/version transitions instead of a generic diff label.
diff --git a/report/components/ConfigChangesSection.tsx b/report/components/ConfigChangesSection.tsx index 92010ac6c..e5791c11b 100644 --- a/report/components/ConfigChangesSection.tsx +++ b/report/components/ConfigChangesSection.tsx @@ -55,11 +55,11 @@ function getChangeAccent(change: ConfigChange, label: string): ChangeBadgeStyle if (kind === 'Screenshot/v1' || type.includes('screenshot')) return CHANGE_BADGE_STYLES.artifact; if (kind === 'PermissionChange/v1' || category.startsWith('rbac') || type.includes('permission')) return CHANGE_BADGE_STYLES.permission; - if (kind === 'Backup/v1' || category.startsWith('backup') || type.includes('backup') || type.includes('restore')) return CHANGE_BADGE_STYLES.backup; + if (kind === 'Backup/v1' || kind === 'Restore/v1' || category.startsWith('backup') || type.includes('backup') || type.includes('restore')) return CHANGE_BADGE_STYLES.backup; if (kind === 'CostChange/v1' || type.includes('cost')) return CHANGE_BADGE_STYLES.cost; - if (kind === 'Promotion/v1' || kind === 'Rollback/v1' || kind === 'PipelineRun/v1' || kind === 'PlaybookExecution/v1') return CHANGE_BADGE_STYLES.release; - if (kind === 'Scaling/v1' || type.includes('replica') || type.includes('scaling')) return CHANGE_BADGE_STYLES.scale; - if (kind === 'Deployment/v1' || type === 'diff' || category.startsWith('deployment')) return CHANGE_BADGE_STYLES.diff; + if (kind === 'Promotion/v1' || kind === 'Approval/v1' || kind === 'Rollback/v1' || kind === 'PipelineRun/v1' || kind === 'PlaybookExecution/v1') return CHANGE_BADGE_STYLES.release; + if (kind === 'Scale/v1' || kind === 'Scaling/v1' || type.includes('replica') || type.includes('scaling')) return CHANGE_BADGE_STYLES.scale; + if (kind === 'ConfigChange/v1' || kind === 'Change/v1' || kind === 'Deployment/v1' || type === 'diff' || category.startsWith('deployment')) return CHANGE_BADGE_STYLES.diff; if (type.includes('policy') || normalizedLabel.includes('policy')) return CHANGE_BADGE_STYLES.policy; return CHANGE_BADGE_STYLES.default; } diff --git a/report/components/change-section-utils.ts b/report/components/change-section-utils.ts index f0cd527aa..7114b7d10 100644 --- a/report/components/change-section-utils.ts +++ b/report/components/change-section-utils.ts @@ -479,10 +479,22 @@ function asText(value: unknown): string | undefined { return undefined; } +function asRecord(value: unknown): Record | undefined { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return value as Record; + } + return undefined; +} + function compactMeta(values: Array): string[] { return values.filter((value): value is string => Boolean(value)); } +function joinText(values: Array, separator = ', '): string | undefined { + const filtered = compactMeta(values); + return filtered.length > 0 ? filtered.join(separator) : undefined; +} + function labelValue(label: string, value: unknown): string | undefined { const text = asText(value); return text ? `${label}: ${text}` : undefined; @@ -539,6 +551,131 @@ function formatCurrencyAmount(value: unknown, currency: unknown): string | undef return String(value); } +function identityLabel(value: unknown): string | undefined { + const record = asRecord(value); + if (!record) { + return undefined; + } + return asText(record.name) || asText(record.id) || asText(record.type); +} + +function environmentLabel(value: unknown): string | undefined { + const record = asRecord(value); + if (!record) { + return undefined; + } + return asText(record.name) || asText(record.identifier); +} + +function dimensionLabel(value: unknown): string | undefined { + const record = asRecord(value); + if (!record) { + return asText(value); + } + + const desired = asText(record.desired); + if (desired) { + return desired; + } + + const min = asText(record.min); + const max = asText(record.max); + if (min || max) { + return joinText([min, max], '..'); + } + + return undefined; +} + +function formatObjectPreview(value: unknown): string | undefined { + const record = asRecord(value); + if (record) { + const entries = Object.entries(record); + if (entries.length === 1) { + const [key, nested] = entries[0]; + const nestedText = asText(nested) || formatObjectPreview(nested); + return nestedText ? `${key}: ${nestedText}` : key; + } + + try { + return JSON.stringify(record); + } catch { + return undefined; + } + } + + if (Array.isArray(value)) { + try { + return JSON.stringify(value); + } catch { + return undefined; + } + } + + return asText(value); +} + +function arrayCountLabel(label: string, value: unknown): string | undefined { + return Array.isArray(value) && value.length > 0 ? `${label}: ${value.length}` : undefined; +} + +function objectCountLabel(label: string, value: unknown): string | undefined { + const record = asRecord(value); + return record && Object.keys(record).length > 0 ? `${label}: ${Object.keys(record).length}` : undefined; +} + +function sourceSummary(value: unknown): string | undefined { + const source = asRecord(value); + if (!source) { + return undefined; + } + + const git = asRecord(source.git) ?? asRecord(source.kustomization) ?? asRecord(source.argocd); + if (git) { + return joinText(['Git', asText(git.url) || asText(git.branch) || asText(git.commit_sha)], ': '); + } + + const helm = asRecord(source.helm); + if (helm) { + return joinText(['Helm', asText(helm.chart_name) || asText(helm.repo_url)], ': '); + } + + const image = asRecord(source.image); + if (image) { + const imageRef = joinText([asText(image.registry), asText(image.image)], '/'); + return joinText(['Image', imageRef || asText(image.version)], ': '); + } + + const database = asRecord(source.database); + if (database) { + return joinText(['Database', asText(database.name) || asText(database.endpoint)], ': '); + } + + const other = asText(source.other); + if (other) { + return joinText(['Other', other], ': '); + } + + return undefined; +} + +function changePathsLabel(value: unknown): string | undefined { + if (!Array.isArray(value)) { + return undefined; + } + + const paths = value + .map((item) => asText(asRecord(item)?.path)) + .filter((item): item is string => Boolean(item)); + + if (!paths.length) { + return undefined; + } + + const preview = paths.slice(0, 2).join(', '); + return `Paths: ${preview}${paths.length > 2 ? ` +${paths.length - 2} more` : ''}`; +} + function humanizeLabel(value: string): string { return value .replace(/[_-]+/g, ' ') @@ -581,8 +718,8 @@ function permissionFromTypedChange(typedChange?: ConfigTypedChange): Application const TYPED_CHANGE_RENDERERS: Record Omit> = { 'UserChange/v1': (typedChange) => ({ + summary: asText(typedChange.user_name) || asText(typedChange.user_id), meta: compactMeta([ - asText(typedChange.user_name) || asText(typedChange.user_id), asText(typedChange.user_email), labelValue('Group', typedChange.group_name || typedChange.group_id), labelValue('Type', typedChange.user_type), @@ -590,6 +727,7 @@ const TYPED_CHANGE_RENDERERS: Record ]), }), 'Screenshot/v1': (typedChange) => ({ + summary: asText(typedChange.url) || asText(typedChange.artifact_id), meta: compactMeta([ labelValue('Artifact', typedChange.artifact_id), labelValue('Type', typedChange.content_type), @@ -598,16 +736,81 @@ const TYPED_CHANGE_RENDERERS: Record ]), }), 'PermissionChange/v1': (typedChange) => ({ + summary: asText(typedChange.user_name) || asText(typedChange.group_name) || asText(typedChange.user_id) || asText(typedChange.group_id), meta: compactMeta([ - asText(typedChange.user_name) || asText(typedChange.group_name) || asText(typedChange.user_id) || asText(typedChange.group_id), labelValue('Role', typedChange.role_name || typedChange.role_id), labelValue('Role Type', typedChange.role_type), labelValue('Scope', typedChange.scope), ]), }), + 'Identity/v1': (typedChange) => ({ + summary: identityLabel(typedChange), + meta: compactMeta([ + labelValue('Type', typedChange.type), + labelValue('Comment', typedChange.comment), + ]), + }), + 'GitSource/v1': (typedChange) => ({ + summary: asText(typedChange.url), + meta: compactMeta([ + labelValue('Branch', typedChange.branch), + labelValue('Commit', typedChange.commit_sha), + labelValue('Version', typedChange.version), + labelValue('Tags', typedChange.tags), + ]), + }), + 'HelmSource/v1': (typedChange) => ({ + summary: asText(typedChange.chart_name), + meta: compactMeta([ + labelValue('Version', typedChange.chart_version), + labelValue('Repo', typedChange.repo_url), + ]), + }), + 'ImageSource/v1': (typedChange) => ({ + summary: joinText([asText(typedChange.registry), asText(typedChange.image)], '/'), + meta: compactMeta([ + labelValue('Version', typedChange.version), + labelValue('SHA', typedChange.sha), + ]), + }), + 'DatabaseSource/v1': (typedChange) => ({ + summary: asText(typedChange.name) || asText(typedChange.endpoint), + meta: compactMeta([ + labelValue('Type', typedChange.type), + labelValue('Schema', typedChange.schema), + labelValue('Version', typedChange.version), + labelValue('Endpoint', typedChange.endpoint), + ]), + }), + 'Source/v1': (typedChange) => ({ + summary: sourceSummary(typedChange), + meta: compactMeta([ + labelValue('Path', typedChange.path), + labelValue('Other', typedChange.other), + ]), + }), + 'Environment/v1': (typedChange) => ({ + summary: environmentLabel(typedChange), + meta: compactMeta([ + labelValue('Type', typedChange.type), + labelValue('Stage', typedChange.stage), + labelValue('Identifier', typedChange.identifier), + objectCountLabel('Tags', typedChange.tags), + ]), + }), + 'Event/v1': (typedChange) => ({ + summary: asText(typedChange.id), + meta: compactMeta([ + labelValue('URL', typedChange.url), + labelValue('Timestamp', typedChange.timestamp), + objectCountLabel('Tags', typedChange.tags), + objectCountLabel('Properties', typedChange.properties), + ]), + }), 'Deployment/v1': (typedChange) => { const imageDiff = toDiff('Image', typedChange.previous_image, typedChange.new_image); return { + summary: asText(typedChange.container), meta: compactMeta([ labelValue('Container', typedChange.container), imageDiff ? undefined : transition('Image', typedChange.previous_image, typedChange.new_image), @@ -618,24 +821,47 @@ const TYPED_CHANGE_RENDERERS: Record }; }, 'Promotion/v1': (typedChange) => { - const environmentDiff = toDiff('Environment', typedChange.from_environment, typedChange.to_environment); + const fromEnvironment = environmentLabel(typedChange.from) || asText(typedChange.from_environment); + const toEnvironment = environmentLabel(typedChange.to) || asText(typedChange.to_environment); + const environmentDiff = toDiff('Environment', fromEnvironment, toEnvironment); return { + summary: asText(typedChange.artifact) || asText(typedChange.version), meta: compactMeta([ - environmentDiff ? undefined : transition('Environment', typedChange.from_environment, typedChange.to_environment), + environmentDiff ? undefined : transition('Environment', fromEnvironment, toEnvironment), labelValue('Version', typedChange.version), labelValue('Artifact', typedChange.artifact), + labelValue('Source', sourceSummary(typedChange.source)), + arrayCountLabel('Approvals', typedChange.approvals), ]), diff: environmentDiff, }; }, - 'Approval/v1': (typedChange) => ({ - summary: asText(typedChange.approved_by) ? `Approved by ${typedChange.approved_by}` : asText(typedChange.rejected_by) ? `Rejected by ${typedChange.rejected_by}` : 'Approval decision', - meta: compactMeta([ - labelValue('Playbook', typedChange.playbook_id), - labelValue('Run', typedChange.run_id), - labelValue('Reason', typedChange.reason), - ]), - }), + 'Approval/v1': (typedChange) => { + const submittedBy = identityLabel(typedChange.submitted_by) || asText(typedChange.submitted_by); + const approver = identityLabel(typedChange.approver) || asText(typedChange.approved_by) || asText(typedChange.rejected_by); + const status = asText(typedChange.status) + || (asText(typedChange.approved_by) ? 'Approved' : undefined) + || (asText(typedChange.rejected_by) ? 'Rejected' : undefined); + const summary = approver && status + ? `${status} by ${approver}` + : submittedBy + ? `Submitted by ${submittedBy}` + : status + ? `${status} approval` + : 'Approval decision'; + return { + summary, + meta: compactMeta([ + labelValue('Submitted By', submittedBy), + labelValue('Approver', approver), + labelValue('Stage', typedChange.stage), + labelValue('Status', status), + labelValue('Playbook', typedChange.playbook_id), + labelValue('Run', typedChange.run_id), + labelValue('Reason', typedChange.reason), + ]), + }; + }, 'Rollback/v1': (typedChange) => { const versionDiff = toDiff('Version', typedChange.from_version, typedChange.to_version); return { @@ -648,12 +874,17 @@ const TYPED_CHANGE_RENDERERS: Record }; }, 'Backup/v1': (typedChange) => ({ + summary: environmentLabel(typedChange.environment) || asText(typedChange.target) || asText(typedChange.backup_type), meta: compactMeta([ labelValue('Status', typedChange.status), labelValue('Type', typedChange.backup_type), + labelValue('Created By', identityLabel(typedChange.created_by)), + labelValue('Environment', environmentLabel(typedChange.environment)), labelValue('Target', typedChange.target), labelValue('Size', typedChange.size), + labelValue('Delta', typedChange.delta), labelValue('Duration', typedChange.duration), + labelValue('End', typedChange.end), labelValue('Snapshot', typedChange.snapshot_id), ]), }), @@ -672,6 +903,7 @@ const TYPED_CHANGE_RENDERERS: Record 'Scaling/v1': (typedChange) => { const replicaDiff = toDiff('Replicas', typedChange.from_replicas, typedChange.to_replicas); return { + summary: asText(typedChange.resource_type), meta: compactMeta([ labelValue('Resource', typedChange.resource_type), replicaDiff ? undefined : transition('Replicas', typedChange.from_replicas, typedChange.to_replicas), @@ -680,6 +912,19 @@ const TYPED_CHANGE_RENDERERS: Record diff: replicaDiff, }; }, + 'Scale/v1': (typedChange) => { + const previousValue = dimensionLabel(typedChange.previous_value); + const currentValue = dimensionLabel(typedChange.value); + const label = asText(typedChange.dimension) || 'Value'; + const scaleDiff = toDiff(label, previousValue, currentValue); + return { + summary: typedChange.dimension ? `${typedChange.dimension} scaling` : 'Scale change', + meta: compactMeta([ + scaleDiff ? undefined : transition(label, previousValue, currentValue), + ]), + diff: scaleDiff, + }; + }, 'Certificate/v1': (typedChange) => ({ summary: labelValue('Subject', typedChange.subject), meta: compactMeta([ @@ -705,18 +950,73 @@ const TYPED_CHANGE_RENDERERS: Record }; }, 'PipelineRun/v1': (typedChange) => { - const pipeline = asText(typedChange.pipeline_name) || asText(typedChange.pipeline_id); + const pipeline = asText(typedChange.pipeline_name) || asText(typedChange.pipeline_id) || environmentLabel(typedChange.environment); return { summary: pipeline, meta: compactMeta([ labelValue('Run', typedChange.run_number ?? typedChange.run_id), labelValue('Branch', typedChange.branch), + labelValue('Environment', environmentLabel(typedChange.environment)), labelValue('Status', typedChange.status), labelValue('Duration', typedChange.duration), labelValue('Error', typedChange.error), ]), }; }, + 'Change/v1': (typedChange) => { + const changeDiff = toDiff('Value', formatObjectPreview(typedChange.from), formatObjectPreview(typedChange.to)); + return { + summary: asText(typedChange.path) || 'Field change', + meta: compactMeta([ + labelValue('Type', typedChange.type), + changeDiff ? undefined : transition('Value', formatObjectPreview(typedChange.from), formatObjectPreview(typedChange.to)), + ]), + diff: changeDiff, + }; + }, + 'ConfigChange/v1': (typedChange) => { + const changeCount = Array.isArray(typedChange.changes) ? typedChange.changes.length : 0; + return { + summary: changeCount > 0 ? `${changeCount} field change${changeCount === 1 ? '' : 's'}` : 'Config change', + meta: compactMeta([ + labelValue('Author', identityLabel(typedChange.author)), + labelValue('Environment', environmentLabel(typedChange.environment)), + labelValue('Source', sourceSummary(typedChange.source)), + changePathsLabel(typedChange.changes), + ]), + }; + }, + 'Restore/v1': (typedChange) => { + const fromEnvironment = environmentLabel(typedChange.from); + const toEnvironment = environmentLabel(typedChange.to); + const environmentDiff = toDiff('Environment', fromEnvironment, toEnvironment); + return { + summary: sourceSummary(typedChange.source) || asText(typedChange.status) || 'Restore job', + meta: compactMeta([ + environmentDiff ? undefined : transition('Environment', fromEnvironment, toEnvironment), + labelValue('Source', sourceSummary(typedChange.source)), + labelValue('Status', typedChange.status), + ]), + diff: environmentDiff, + }; + }, + 'Test/v1': (typedChange) => ({ + summary: asText(typedChange.name) || asText(typedChange.id), + meta: compactMeta([ + labelValue('Type', typedChange.type), + labelValue('Status', typedChange.status), + labelValue('Result', typedChange.result), + labelValue('Description', typedChange.description), + ]), + }), + 'Dimension/v1': (typedChange) => ({ + summary: dimensionLabel(typedChange), + meta: compactMeta([ + labelValue('Min', typedChange.min), + labelValue('Max', typedChange.max), + labelValue('Desired', typedChange.desired), + ]), + }), }; export function getTypedChangeDisplay(change: ConfigChange): TypedChangeDisplay | undefined { diff --git a/report/kitchen-sink-data.ts b/report/kitchen-sink-data.ts index 1779c058b..fb979e157 100644 --- a/report/kitchen-sink-data.ts +++ b/report/kitchen-sink-data.ts @@ -1,12 +1,11 @@ import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; -import yaml from 'js-yaml'; import type { KitchenSinkData } from './kitchen-sink/KitchenSinkTypes.ts'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const raw = readFileSync(resolve(__dirname, 'testdata/kitchen-sink.yaml'), 'utf-8'); -export const data = yaml.load(raw) as KitchenSinkData; +const raw = readFileSync(resolve(__dirname, 'kitchen-sink.json'), 'utf-8'); +export const data = JSON.parse(raw) as KitchenSinkData; export default data; diff --git a/report/kitchen-sink.json b/report/kitchen-sink.json new file mode 100644 index 000000000..5baf9728b --- /dev/null +++ b/report/kitchen-sink.json @@ -0,0 +1,3806 @@ +{ + "configItem": { + "id": "cfg-eks-001", + "name": "prod-eks-cluster", + "type": "AWS::EKS::Cluster", + "configClass": "Cluster", + "status": "Active", + "health": "healthy", + "description": "Production EKS cluster running Mission Control workloads in us-east-1", + "labels": { + "env": "production", + "team": "platform", + "region": "us-east-1" + }, + "costTotal30d": 4280.5, + "createdAt": "2025-03-15T09:00:00Z", + "updatedAt": "2026-03-28T12:00:00Z" + }, + "categoryMappings": [ + { + "category": "rbac.granted", + "filter": "changeType == \"PermissionGranted\" || changeType == \"PermissionAdded\" || changeType == \"IAMRoleAdded\"" + }, + { + "category": "rbac.revoked", + "filter": "changeType == \"PermissionRevoked\" || changeType == \"PermissionRemoved\" || changeType == \"IAMRoleRemoved\"" + }, + { + "category": "backup.success", + "filter": "changeType == \"BackupCompleted\" || changeType == \"BackupSuccessful\"" + }, + { + "category": "backup.failed", + "filter": "changeType == \"BackupFailed\"" + }, + { + "category": "backup.progress", + "filter": "changeType == \"BackupStarted\" || changeType == \"BackupRunning\" || changeType == \"BackupEnqueued\"" + }, + { + "category": "backup.restore", + "filter": "changeType == \"BackupRestored\" || changeType == \"RestoreCompleted\"" + }, + { + "category": "deployment.spec", + "filter": "changeType == \"diff\"" + }, + { + "category": "deployment.scale", + "filter": "changeType == \"ScalingReplicaSet\"" + }, + { + "category": "deployment.policy", + "filter": "changeType == \"PolicyUpdate\"" + } + ], + "changes": [ + { + "id": "chg-001", + "configID": "cfg-eks-001", + "changeType": "diff", + "category": "deployment.spec", + "severity": "info", + "source": "kubernetes", + "summary": "Node pool autoscaler adjusted desired count from 3 to 5", + "createdBy": "cluster-autoscaler", + "createdAt": "2026-03-30T08:15:00Z", + "count": 1 + }, + { + "id": "chg-002", + "configID": "cfg-eks-001", + "changeType": "Pulled", + "severity": "info", + "source": "kubernetes", + "summary": "Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42", + "createdAt": "2026-03-30T07:30:00Z", + "count": 3 + }, + { + "id": "chg-003", + "configID": "cfg-eks-001", + "changeType": "ScalingReplicaSet", + "category": "deployment.scale", + "severity": "low", + "source": "kubernetes", + "summary": "Deployment incident-commander scaled from 2 to 3 replicas", + "externalCreatedBy": "hpa-controller", + "createdAt": "2026-03-29T22:00:00Z" + }, + { + "id": "chg-004", + "configID": "cfg-eks-001", + "changeType": "diff", + "category": "deployment.spec", + "severity": "medium", + "source": "terraform", + "summary": "EKS cluster version upgraded from 1.28 to 1.29", + "createdBy": "alice@flanksource.com", + "createdAt": "2026-03-29T14:00:00Z" + }, + { + "id": "chg-005", + "configID": "cfg-eks-001", + "changeType": "PolicyUpdate", + "category": "deployment.policy", + "severity": "high", + "source": "argocd", + "summary": "Network policy updated: restricted egress to 10.0.0.0/8 for namespace mc", + "createdBy": "bob@flanksource.com", + "createdAt": "2026-03-28T16:00:00Z" + }, + { + "id": "chg-006", + "configID": "cfg-eks-001", + "changeType": "diff", + "category": "deployment.spec", + "severity": "critical", + "source": "aws-config", + "summary": "IAM role policy detached: eks-admin-access removed from cluster role", + "createdBy": "security-automation", + "createdAt": "2026-03-28T10:00:00Z" + }, + { + "id": "chg-007", + "configID": "cfg-eks-001", + "changeType": "FieldsV1", + "severity": "info", + "source": "kubernetes", + "summary": "ConfigMap kube-proxy updated with new CIDR ranges", + "createdAt": "2026-03-27T18:00:00Z", + "count": 2 + }, + { + "id": "chg-008", + "configID": "cfg-eks-001", + "changeType": "diff", + "severity": "low", + "source": "terraform", + "summary": "Added tag cost-center=platform-engineering to cluster", + "createdBy": "carol@flanksource.com", + "createdAt": "2026-03-27T09:00:00Z" + }, + { + "id": "chg-009", + "configID": "cfg-eks-001", + "changeType": "ScalingReplicaSet", + "category": "deployment.scale", + "severity": "info", + "source": "kubernetes", + "summary": "Deployment canary-checker scaled from 1 to 2 replicas", + "externalCreatedBy": "hpa-controller", + "createdAt": "2026-03-26T20:00:00Z" + }, + { + "id": "chg-010", + "configID": "cfg-eks-001", + "changeType": "diff", + "category": "deployment.spec", + "severity": "medium", + "source": "argocd", + "summary": "Helm release cert-manager upgraded from v1.13.3 to v1.14.1", + "createdBy": "alice@flanksource.com", + "createdAt": "2026-03-26T11:00:00Z" + }, + { + "id": "chg-011", + "configID": "cfg-eks-001", + "changeType": "Pulled", + "severity": "info", + "source": "kubernetes", + "summary": "Image flanksource/canary-checker:v1.0.350 pulled", + "createdAt": "2026-03-25T15:00:00Z", + "count": 5 + }, + { + "id": "chg-012", + "configID": "cfg-eks-001", + "changeType": "PolicyUpdate", + "category": "deployment.policy", + "severity": "high", + "source": "aws-config", + "summary": "Security group sg-0abc123 ingress rule added: allow 443 from 0.0.0.0/0", + "createdBy": "terraform", + "createdAt": "2026-03-25T10:00:00Z" + }, + { + "id": "chg-013", + "configID": "cfg-eks-001", + "changeType": "diff", + "category": "deployment.spec", + "severity": "low", + "source": "kubernetes", + "summary": "PodDisruptionBudget added for incident-commander (minAvailable: 2)", + "createdBy": "bob@flanksource.com", + "createdAt": "2026-03-24T14:00:00Z" + }, + { + "id": "chg-014", + "configID": "cfg-eks-001", + "changeType": "PermissionGranted", + "category": "rbac.granted", + "severity": "info", + "source": "okta", + "summary": "Granted db_owner to alice@flanksource.com on prod-rds-01", + "createdBy": "admin@flanksource.com", + "createdAt": "2026-03-28T09:00:00Z", + "details": { + "permission": { + "user": "alice@flanksource.com", + "role": "db_owner" + } + } + }, + { + "id": "chg-015", + "configID": "cfg-eks-001", + "changeType": "PermissionRevoked", + "category": "rbac.revoked", + "severity": "info", + "source": "okta", + "summary": "Revoked Secrets Reader access for bob@flanksource.com on prod-eks-cluster", + "createdBy": "admin@flanksource.com", + "createdAt": "2026-03-27T15:00:00Z", + "details": { + "permission": { + "user": "bob@flanksource.com", + "role": "Secrets Reader" + } + } + }, + { + "id": "chg-016", + "configID": "cfg-eks-001", + "changeType": "BackupCompleted", + "category": "backup.success", + "severity": "info", + "source": "velero", + "summary": "Full cluster backup completed successfully (2.4 GiB)", + "createdAt": "2026-03-29T03:00:00Z" + }, + { + "id": "chg-017", + "configID": "cfg-eks-001", + "changeType": "BackupFailed", + "category": "backup.failed", + "severity": "high", + "source": "velero", + "summary": "Incremental backup failed: PVC snapshot timeout after 300s", + "createdAt": "2026-03-28T03:00:00Z" + }, + { + "id": "chg-018", + "configID": "cfg-eks-001", + "changeType": "BackupStarted", + "category": "backup.progress", + "severity": "info", + "source": "velero", + "summary": "Scheduled backup initiated for prod-eks-cluster", + "createdAt": "2026-03-30T03:00:00Z" + }, + { + "id": "chg-019", + "configID": "cfg-eks-001", + "changeType": "UserCreated", + "severity": "info", + "source": "okta", + "createdAt": "2026-03-24T11:30:00Z", + "typedChange": { + "kind": "UserChange/v1", + "user_name": "alice", + "user_email": "alice@flanksource.com", + "user_type": "human", + "group_name": "platform-admins", + "tenant": "production" + } + }, + { + "id": "chg-020", + "configID": "cfg-eks-001", + "changeType": "Screenshot", + "severity": "info", + "source": "synthetics", + "createdAt": "2026-03-24T10:15:00Z", + "typedChange": { + "kind": "Screenshot/v1", + "artifact_id": "art-001", + "content_type": "image/png", + "width": 1440, + "height": 900, + "url": "https://prod-eks-cluster.example.com/login" + } + }, + { + "id": "chg-021", + "configID": "cfg-eks-001", + "changeType": "PermissionSync", + "severity": "low", + "source": "iam-reconciler", + "createdAt": "2026-03-24T09:45:00Z", + "typedChange": { + "kind": "PermissionChange/v1", + "user_name": "jane@flanksource.com", + "role_name": "cluster-admin", + "scope": "namespace/mc" + } + }, + { + "id": "chg-022", + "configID": "cfg-eks-001", + "changeType": "Deployment", + "severity": "info", + "source": "argocd", + "createdAt": "2026-03-24T09:00:00Z", + "typedChange": { + "kind": "Deployment/v1", + "previous_image": "flanksource/incident-commander:v1.4.190", + "new_image": "flanksource/incident-commander:v1.4.200", + "container": "incident-commander", + "namespace": "mc", + "strategy": "rolling" + } + }, + { + "id": "chg-023", + "configID": "cfg-eks-001", + "changeType": "Promotion", + "severity": "info", + "source": "release-bot", + "createdAt": "2026-03-24T08:30:00Z", + "typedChange": { + "kind": "Promotion/v1", + "from_environment": "staging", + "to_environment": "production", + "version": "v1.4.200", + "artifact": "incident-commander" + } + }, + { + "id": "chg-024", + "configID": "cfg-eks-001", + "changeType": "Approved", + "severity": "info", + "source": "playbooks", + "createdAt": "2026-03-24T08:00:00Z", + "typedChange": { + "kind": "Approval/v1", + "playbook_id": "pb-001", + "run_id": "run-approve-001", + "approved_by": "ops-lead@flanksource.com", + "reason": "Change window approved" + } + }, + { + "id": "chg-025", + "configID": "cfg-eks-001", + "changeType": "Rollback", + "severity": "high", + "source": "argocd", + "createdAt": "2026-03-24T07:30:00Z", + "typedChange": { + "kind": "Rollback/v1", + "from_version": "v1.4.200", + "to_version": "v1.4.190", + "trigger": "health-check", + "reason": "Elevated error rate" + } + }, + { + "id": "chg-026", + "configID": "cfg-eks-001", + "changeType": "BackupArchived", + "severity": "info", + "source": "velero", + "createdAt": "2026-03-24T07:00:00Z", + "typedChange": { + "kind": "Backup/v1", + "status": "completed", + "backup_type": "full", + "size": "2.4 GiB", + "duration": "4m12s", + "target": "s3://velero-prod", + "snapshot_id": "snap-019" + } + }, + { + "id": "chg-027", + "configID": "cfg-eks-001", + "changeType": "PlaybookCompleted", + "severity": "info", + "source": "playbooks", + "createdAt": "2026-03-24T06:30:00Z", + "typedChange": { + "kind": "PlaybookExecution/v1", + "playbook_name": "Restart Incident Commander", + "run_id": "pb-run-019", + "status": "completed", + "duration": "2m11s" + } + }, + { + "id": "chg-028", + "configID": "cfg-eks-001", + "changeType": "Scaling", + "severity": "low", + "source": "keda", + "createdAt": "2026-03-24T06:00:00Z", + "typedChange": { + "kind": "Scaling/v1", + "from_replicas": 2, + "to_replicas": 4, + "resource_type": "Deployment", + "trigger": "queue-depth" + } + }, + { + "id": "chg-029", + "configID": "cfg-eks-001", + "changeType": "CertificateRenewed", + "severity": "info", + "source": "cert-manager", + "createdAt": "2026-03-24T05:30:00Z", + "typedChange": { + "kind": "Certificate/v1", + "subject": "prod-eks-cluster.internal", + "issuer": "letsencrypt-prod", + "not_after": "2026-06-22T00:00:00Z", + "serial": "09AF23", + "dns_names": "prod-eks-cluster.internal,api.prod.example.com" + } + }, + { + "id": "chg-030", + "configID": "cfg-eks-001", + "changeType": "CostChange", + "severity": "medium", + "source": "cost-analyzer", + "createdAt": "2026-03-24T05:00:00Z", + "typedChange": { + "kind": "CostChange/v1", + "previous_cost": 4280.5, + "new_cost": 4631.25, + "currency": "USD", + "period": "30d", + "reason": "Node group scale-out" + } + }, + { + "id": "chg-031", + "configID": "cfg-eks-001", + "changeType": "PipelineRunCompleted", + "severity": "info", + "source": "github-actions", + "createdAt": "2026-03-24T04:30:00Z", + "typedChange": { + "kind": "PipelineRun/v1", + "pipeline_name": "deploy-incident-commander", + "run_number": 841, + "branch": "main", + "status": "completed", + "duration": "9m31s" + } + }, + { + "id": "schema-example-001", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "UserChange", + "severity": "low", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:59:00.000Z", + "count": 1, + "typedChange": { + "kind": "UserChange/v1", + "user_id": "user-123", + "user_name": "alice@example.com", + "user_email": "alice@example.com", + "user_type": "human", + "group_id": "group-platform", + "group_name": "platform-admins", + "tenant": "acme" + } + }, + { + "id": "schema-example-002", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "PermissionChange", + "severity": "low", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:58:00.000Z", + "count": 1, + "typedChange": { + "kind": "PermissionChange/v1", + "user_id": "user-123", + "user_name": "alice@example.com", + "role_id": "role-admin", + "role_name": "cluster-admin", + "role_type": "kubernetes", + "scope": "prod-cluster" + } + }, + { + "id": "schema-example-003", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Promotion", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:57:00.000Z", + "count": 1, + "typedChange": { + "kind": "Promotion/v1", + "id": "evt-promo-1", + "timestamp": "2026-04-10T12:00:00Z", + "from": { + "kind": "Environment/v1", + "name": "staging", + "stage": "Staging" + }, + "to": { + "kind": "Environment/v1", + "name": "production", + "stage": "Production" + }, + "source": { + "kind": "Source/v1", + "git": { + "kind": "GitSource/v1", + "url": "https://github.com/flanksource/duty.git", + "branch": "main", + "commit_sha": "abc123def456" + }, + "path": "deploy/production" + }, + "version": "v1.2.3", + "approvals": [ + { + "kind": "Approval/v1", + "id": "approval-1", + "submitted_by": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "approver": { + "kind": "Identity/v1", + "id": "user-456", + "type": "User", + "name": "bob@example.com" + }, + "stage": "Manual", + "status": "Approved" + } + ], + "artifact": "api" + } + }, + { + "id": "schema-example-004", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "BackupCompleted", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:56:00.000Z", + "count": 1, + "typedChange": { + "kind": "Backup/v1", + "id": "backup-42", + "timestamp": "2026-04-10T11:30:00Z", + "backup_type": "Snapshot", + "created_by": { + "kind": "Identity/v1", + "type": "System:Auto", + "name": "nightly-backup" + }, + "environment": { + "kind": "Environment/v1", + "name": "production", + "type": "Kubernetes", + "stage": "Production" + }, + "end": "2026-04-10T11:36:12Z", + "status": "Completed", + "size": "4.2GB", + "delta": "275MB" + } + }, + { + "id": "schema-example-005", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "diff", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:55:00.000Z", + "count": 1, + "typedChange": { + "kind": "ConfigChange/v1", + "id": "evt-config-1", + "timestamp": "2026-04-10T12:05:00Z", + "author": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "changes": [ + { + "kind": "Change/v1", + "path": ".spec.template.spec.containers[0].image", + "from": { + "image": "ghcr.io/flanksource/duty:v1.2.2" + }, + "to": { + "image": "ghcr.io/flanksource/duty:v1.2.3" + }, + "type": "update" + }, + { + "kind": "Change/v1", + "path": ".spec.replicas", + "from": { + "desired": "2" + }, + "to": { + "desired": "3" + }, + "type": "update" + } + ], + "environment": { + "kind": "Environment/v1", + "name": "production", + "type": "Kubernetes", + "stage": "Production", + "identifier": "prod-cluster-1" + }, + "source": { + "kind": "Source/v1", + "image": { + "kind": "ImageSource/v1", + "registry": "ghcr.io", + "image": "flanksource/duty", + "version": "v1.2.3", + "sha": "sha256:1234abcd" + } + } + } + }, + { + "id": "schema-example-006", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Screenshot", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:54:00.000Z", + "count": 1, + "typedChange": { + "kind": "Screenshot/v1", + "artifact_id": "artifact-789", + "url": "https://artifacts.example.com/screenshots/login-flow.png", + "content_type": "image/png", + "width": 1920, + "height": 1080 + } + }, + { + "id": "schema-example-007", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "GroupMembership", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:53:00.000Z", + "count": 1, + "typedChange": { + "kind": "GroupMembership/v1", + "group": { + "kind": "Identity/v1", + "id": "group-platform", + "type": "Group", + "name": "platform-admins" + }, + "member": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "action": "Added", + "tenant": "acme" + } + }, + { + "id": "schema-example-008", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Identity", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:52:00.000Z", + "count": 1, + "typedChange": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + } + }, + { + "id": "schema-example-009", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Approved", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:51:00.000Z", + "count": 1, + "typedChange": { + "kind": "Approval/v1", + "id": "approval-42", + "timestamp": "2026-04-10T12:10:00Z", + "submitted_by": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "approver": { + "kind": "Identity/v1", + "id": "user-456", + "type": "User", + "name": "bob@example.com" + }, + "stage": "Manual", + "status": "Approved" + } + }, + { + "id": "schema-example-010", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "GitSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:50:00.000Z", + "count": 1, + "typedChange": { + "kind": "GitSource/v1", + "url": "https://github.com/flanksource/duty.git", + "branch": "main", + "commit_sha": "abc123def456", + "version": "v1.2.3", + "tags": "release,v1.2.3" + } + }, + { + "id": "schema-example-011", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "HelmSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:49:00.000Z", + "count": 1, + "typedChange": { + "kind": "HelmSource/v1", + "chart_name": "mission-control", + "chart_version": "0.42.0", + "repo_url": "https://flanksource.github.io/charts" + } + }, + { + "id": "schema-example-012", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "ImageSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:48:00.000Z", + "count": 1, + "typedChange": { + "kind": "ImageSource/v1", + "registry": "ghcr.io", + "image": "flanksource/duty", + "version": "v1.2.3", + "sha": "sha256:1234abcd" + } + }, + { + "id": "schema-example-013", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "DatabaseSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:47:00.000Z", + "count": 1, + "typedChange": { + "kind": "DatabaseSource/v1", + "type": "PostgreSQL", + "name": "incidents", + "schema": "public", + "version": "15.4", + "endpoint": "incidents.cluster-abc.us-east-1.rds.amazonaws.com:5432" + } + }, + { + "id": "schema-example-014", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Source", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:46:00.000Z", + "count": 1, + "typedChange": { + "kind": "Source/v1", + "git": { + "kind": "GitSource/v1", + "url": "https://github.com/flanksource/duty.git", + "branch": "main", + "commit_sha": "abc123def456" + }, + "path": "deploy/production" + } + }, + { + "id": "schema-example-015", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Environment", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:45:00.000Z", + "count": 1, + "typedChange": { + "kind": "Environment/v1", + "name": "production", + "description": "Primary production cluster", + "type": "Kubernetes", + "stage": "Production", + "identifier": "prod-cluster-1", + "tags": { + "cost_center": "eng", + "team": "platform" + } + } + }, + { + "id": "schema-example-016", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Event", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:44:00.000Z", + "count": 1, + "typedChange": { + "kind": "Event/v1", + "id": "evt-9", + "url": "https://events.example.com/evt-9", + "tags": { + "source": "ci" + }, + "timestamp": "2026-04-10T12:15:00Z" + } + }, + { + "id": "schema-example-017", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Test", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:43:00.000Z", + "count": 1, + "typedChange": { + "kind": "Test/v1", + "id": "test-101", + "timestamp": "2026-04-10T12:20:00Z", + "name": "api-smoke", + "description": "Smoke tests for the public API", + "type": "Integration", + "status": "Passed", + "result": "Passed" + } + }, + { + "id": "schema-example-018", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "PipelineRun", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:42:00.000Z", + "count": 1, + "typedChange": { + "kind": "PipelineRun/v1", + "id": "pipeline-55", + "timestamp": "2026-04-10T12:25:00Z", + "environment": { + "kind": "Environment/v1", + "name": "production", + "type": "Kubernetes", + "stage": "Production" + }, + "status": "Completed" + } + }, + { + "id": "schema-example-019", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Change", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:41:00.000Z", + "count": 1, + "typedChange": { + "kind": "Change/v1", + "path": ".spec.replicas", + "from": { + "desired": "2" + }, + "to": { + "desired": "3" + }, + "type": "update" + } + }, + { + "id": "schema-example-020", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "RestoreCompleted", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:40:00.000Z", + "count": 1, + "typedChange": { + "kind": "Restore/v1", + "id": "restore-7", + "timestamp": "2026-04-10T12:30:00Z", + "from": { + "kind": "Environment/v1", + "name": "backup-store", + "type": "Cloud" + }, + "to": { + "kind": "Environment/v1", + "name": "staging", + "type": "Kubernetes", + "stage": "Staging" + }, + "source": { + "kind": "Source/v1", + "database": { + "kind": "DatabaseSource/v1", + "type": "PostgreSQL", + "name": "incidents", + "version": "15.4" + } + }, + "status": "Completed" + } + }, + { + "id": "schema-example-021", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Dimension", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:39:00.000Z", + "count": 1, + "typedChange": { + "kind": "Dimension/v1", + "min": "2", + "max": "10", + "desired": "5" + } + }, + { + "id": "schema-example-022", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Scaling", + "severity": "low", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:38:00.000Z", + "count": 1, + "typedChange": { + "kind": "Scale/v1", + "dimension": "Replicas", + "previous_value": { + "kind": "Dimension/v1", + "desired": "2" + }, + "value": { + "kind": "Dimension/v1", + "desired": "3" + } + } + }, + { + "id": "schema-example-023", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Approved", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:37:00.000Z", + "count": 1, + "typedChange": { + "kind": "Approval/v1", + "id": "approval-42", + "timestamp": "2026-04-10T12:10:00Z", + "submitted_by": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "approver": { + "kind": "Identity/v1", + "id": "user-456", + "type": "User", + "name": "bob@example.com" + }, + "stage": "Manual", + "status": "Approved" + } + }, + { + "id": "schema-example-024", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "BackupCompleted", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:36:00.000Z", + "count": 1, + "typedChange": { + "kind": "Backup/v1", + "id": "backup-42", + "timestamp": "2026-04-10T11:30:00Z", + "backup_type": "Snapshot", + "created_by": { + "kind": "Identity/v1", + "type": "System:Auto", + "name": "nightly-backup" + }, + "environment": { + "kind": "Environment/v1", + "name": "production", + "type": "Kubernetes", + "stage": "Production" + }, + "end": "2026-04-10T11:36:12Z", + "status": "Completed", + "size": "4.2GB", + "delta": "275MB" + } + }, + { + "id": "schema-example-025", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Change", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:35:00.000Z", + "count": 1, + "typedChange": { + "kind": "Change/v1", + "path": ".spec.replicas", + "from": { + "desired": "2" + }, + "to": { + "desired": "3" + }, + "type": "update" + } + }, + { + "id": "schema-example-026", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "diff", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:34:00.000Z", + "count": 1, + "typedChange": { + "kind": "ConfigChange/v1", + "id": "evt-config-1", + "timestamp": "2026-04-10T12:05:00Z", + "author": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "changes": [ + { + "kind": "Change/v1", + "path": ".spec.template.spec.containers[0].image", + "from": { + "image": "ghcr.io/flanksource/duty:v1.2.2" + }, + "to": { + "image": "ghcr.io/flanksource/duty:v1.2.3" + }, + "type": "update" + }, + { + "kind": "Change/v1", + "path": ".spec.replicas", + "from": { + "desired": "2" + }, + "to": { + "desired": "3" + }, + "type": "update" + } + ], + "environment": { + "kind": "Environment/v1", + "name": "production", + "type": "Kubernetes", + "stage": "Production", + "identifier": "prod-cluster-1" + }, + "source": { + "kind": "Source/v1", + "image": { + "kind": "ImageSource/v1", + "registry": "ghcr.io", + "image": "flanksource/duty", + "version": "v1.2.3", + "sha": "sha256:1234abcd" + } + } + } + }, + { + "id": "schema-example-027", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "DatabaseSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:33:00.000Z", + "count": 1, + "typedChange": { + "kind": "DatabaseSource/v1", + "type": "PostgreSQL", + "name": "incidents", + "schema": "public", + "version": "15.4", + "endpoint": "incidents.cluster-abc.us-east-1.rds.amazonaws.com:5432" + } + }, + { + "id": "schema-example-028", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Dimension", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:32:00.000Z", + "count": 1, + "typedChange": { + "kind": "Dimension/v1", + "min": "2", + "max": "10", + "desired": "5" + } + }, + { + "id": "schema-example-029", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Environment", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:31:00.000Z", + "count": 1, + "typedChange": { + "kind": "Environment/v1", + "name": "production", + "description": "Primary production cluster", + "type": "Kubernetes", + "stage": "Production", + "identifier": "prod-cluster-1", + "tags": { + "cost_center": "eng", + "team": "platform" + } + } + }, + { + "id": "schema-example-030", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Event", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:30:00.000Z", + "count": 1, + "typedChange": { + "kind": "Event/v1", + "id": "evt-9", + "url": "https://events.example.com/evt-9", + "tags": { + "source": "ci" + }, + "timestamp": "2026-04-10T12:15:00Z" + } + }, + { + "id": "schema-example-031", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "GitSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:29:00.000Z", + "count": 1, + "typedChange": { + "kind": "GitSource/v1", + "url": "https://github.com/flanksource/duty.git", + "branch": "main", + "commit_sha": "abc123def456", + "version": "v1.2.3", + "tags": "release,v1.2.3" + } + }, + { + "id": "schema-example-032", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "GroupMembership", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:28:00.000Z", + "count": 1, + "typedChange": { + "kind": "GroupMembership/v1", + "group": { + "kind": "Identity/v1", + "id": "group-platform", + "type": "Group", + "name": "platform-admins" + }, + "member": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "action": "Added", + "tenant": "acme" + } + }, + { + "id": "schema-example-033", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "HelmSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:27:00.000Z", + "count": 1, + "typedChange": { + "kind": "HelmSource/v1", + "chart_name": "mission-control", + "chart_version": "0.42.0", + "repo_url": "https://flanksource.github.io/charts" + } + }, + { + "id": "schema-example-034", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Identity", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:26:00.000Z", + "count": 1, + "typedChange": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + } + }, + { + "id": "schema-example-035", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "ImageSource", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:25:00.000Z", + "count": 1, + "typedChange": { + "kind": "ImageSource/v1", + "registry": "ghcr.io", + "image": "flanksource/duty", + "version": "v1.2.3", + "sha": "sha256:1234abcd" + } + }, + { + "id": "schema-example-036", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "PermissionChange", + "severity": "low", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:24:00.000Z", + "count": 1, + "typedChange": { + "kind": "PermissionChange/v1", + "user_id": "user-123", + "user_name": "alice@example.com", + "role_id": "role-admin", + "role_name": "cluster-admin", + "role_type": "kubernetes", + "scope": "prod-cluster" + } + }, + { + "id": "schema-example-037", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "PipelineRun", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:23:00.000Z", + "count": 1, + "typedChange": { + "kind": "PipelineRun/v1", + "id": "pipeline-55", + "timestamp": "2026-04-10T12:25:00Z", + "environment": { + "kind": "Environment/v1", + "name": "production", + "type": "Kubernetes", + "stage": "Production" + }, + "status": "Completed" + } + }, + { + "id": "schema-example-038", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Promotion", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:22:00.000Z", + "count": 1, + "typedChange": { + "kind": "Promotion/v1", + "id": "evt-promo-1", + "timestamp": "2026-04-10T12:00:00Z", + "from": { + "kind": "Environment/v1", + "name": "staging", + "stage": "Staging" + }, + "to": { + "kind": "Environment/v1", + "name": "production", + "stage": "Production" + }, + "source": { + "kind": "Source/v1", + "git": { + "kind": "GitSource/v1", + "url": "https://github.com/flanksource/duty.git", + "branch": "main", + "commit_sha": "abc123def456" + }, + "path": "deploy/production" + }, + "version": "v1.2.3", + "approvals": [ + { + "kind": "Approval/v1", + "id": "approval-1", + "submitted_by": { + "kind": "Identity/v1", + "id": "user-123", + "type": "User", + "name": "alice@example.com" + }, + "approver": { + "kind": "Identity/v1", + "id": "user-456", + "type": "User", + "name": "bob@example.com" + }, + "stage": "Manual", + "status": "Approved" + } + ], + "artifact": "api" + } + }, + { + "id": "schema-example-039", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "RestoreCompleted", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:21:00.000Z", + "count": 1, + "typedChange": { + "kind": "Restore/v1", + "id": "restore-7", + "timestamp": "2026-04-10T12:30:00Z", + "from": { + "kind": "Environment/v1", + "name": "backup-store", + "type": "Cloud" + }, + "to": { + "kind": "Environment/v1", + "name": "staging", + "type": "Kubernetes", + "stage": "Staging" + }, + "source": { + "kind": "Source/v1", + "database": { + "kind": "DatabaseSource/v1", + "type": "PostgreSQL", + "name": "incidents", + "version": "15.4" + } + }, + "status": "Completed" + } + }, + { + "id": "schema-example-040", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Scaling", + "severity": "low", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:20:00.000Z", + "count": 1, + "typedChange": { + "kind": "Scale/v1", + "dimension": "Replicas", + "previous_value": { + "kind": "Dimension/v1", + "desired": "2" + }, + "value": { + "kind": "Dimension/v1", + "desired": "3" + } + } + }, + { + "id": "schema-example-041", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Screenshot", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:19:00.000Z", + "count": 1, + "typedChange": { + "kind": "Screenshot/v1", + "artifact_id": "artifact-789", + "url": "https://artifacts.example.com/screenshots/login-flow.png", + "content_type": "image/png", + "width": 1920, + "height": 1080 + } + }, + { + "id": "schema-example-042", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Source", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:18:00.000Z", + "count": 1, + "typedChange": { + "kind": "Source/v1", + "git": { + "kind": "GitSource/v1", + "url": "https://github.com/flanksource/duty.git", + "branch": "main", + "commit_sha": "abc123def456" + }, + "path": "deploy/production" + } + }, + { + "id": "schema-example-043", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "Test", + "severity": "info", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:17:00.000Z", + "count": 1, + "typedChange": { + "kind": "Test/v1", + "id": "test-101", + "timestamp": "2026-04-10T12:20:00Z", + "name": "api-smoke", + "description": "Smoke tests for the public API", + "type": "Integration", + "status": "Passed", + "result": "Passed" + } + }, + { + "id": "schema-example-044", + "configID": "schema-example-catalog", + "configName": "Schema Example Catalog", + "configType": "Schema::Example", + "changeType": "UserChange", + "severity": "low", + "source": "schema-examples", + "createdBy": "schema-generator", + "createdAt": "2026-04-10T23:16:00.000Z", + "count": 1, + "typedChange": { + "kind": "UserChange/v1", + "user_id": "user-123", + "user_name": "alice@example.com", + "user_email": "alice@example.com", + "user_type": "human", + "group_id": "group-platform", + "group_name": "platform-admins", + "tenant": "acme" + } + } + ], + "analyses": [ + { + "id": "ana-001", + "configID": "cfg-eks-001", + "analyzer": "Trivy", + "message": "Container image flanksource/incident-commander:v1.4.200 has 3 high CVEs (CVE-2026-1234, CVE-2026-1235, CVE-2026-1236)", + "status": "open", + "severity": "high", + "analysisType": "security", + "source": "trivy-operator", + "firstObserved": "2026-03-28T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-002", + "configID": "cfg-eks-001", + "analyzer": "Trivy", + "message": "Base image golang:1.23-alpine has known vulnerability in libcrypto (CVE-2026-0891)", + "status": "open", + "severity": "critical", + "analysisType": "security", + "source": "trivy-operator", + "firstObserved": "2026-03-25T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-003", + "configID": "cfg-eks-001", + "analyzer": "OPA/Gatekeeper", + "message": "Pod incident-commander-7f8b9c running as root user in namespace mc", + "status": "open", + "severity": "medium", + "analysisType": "compliance", + "source": "gatekeeper", + "firstObserved": "2026-03-20T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-004", + "configID": "cfg-eks-001", + "analyzer": "OPA/Gatekeeper", + "message": "Namespace mc missing required label: data-classification", + "status": "silenced", + "severity": "low", + "analysisType": "compliance", + "source": "gatekeeper", + "firstObserved": "2026-03-15T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-005", + "configID": "cfg-eks-001", + "analyzer": "AWS Cost Optimizer", + "message": "EKS node group i3.xlarge instances are underutilized (avg CPU 18%). Consider downsizing to i3.large.", + "status": "open", + "severity": "medium", + "analysisType": "cost", + "source": "aws-cost-explorer", + "firstObserved": "2026-03-01T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-006", + "configID": "cfg-eks-001", + "analyzer": "AWS Cost Optimizer", + "message": "NAT Gateway data processing charges are 40% above baseline ($320/mo). Review egress traffic patterns.", + "status": "open", + "severity": "low", + "analysisType": "cost", + "source": "aws-cost-explorer", + "firstObserved": "2026-03-10T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-007", + "configID": "cfg-eks-001", + "analyzer": "Prometheus Advisor", + "message": "P99 API response latency exceeded 500ms threshold 12 times in the last 7 days", + "status": "open", + "severity": "high", + "analysisType": "performance", + "source": "prometheus", + "firstObserved": "2026-03-23T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-008", + "configID": "cfg-eks-001", + "analyzer": "AWS Best Practices", + "message": "EKS cluster running version 1.29 - version 1.30 is available with security patches", + "status": "open", + "severity": "info", + "analysisType": "recommendation", + "source": "aws-advisor", + "firstObserved": "2026-03-28T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-009", + "configID": "cfg-eks-001", + "analyzer": "AWS Best Practices", + "message": "Enable EKS control plane logging for audit, authenticator, and scheduler components", + "status": "resolved", + "severity": "medium", + "analysisType": "reliability", + "source": "aws-advisor", + "firstObserved": "2026-02-15T09:00:00Z", + "lastObserved": "2026-03-20T09:00:00Z" + }, + { + "id": "ana-010", + "configID": "cfg-eks-001", + "analyzer": "Prometheus Advisor", + "message": "Node ip-10-0-2-18 memory utilization consistently above 85% - risk of OOM kills", + "status": "open", + "severity": "high", + "analysisType": "reliability", + "source": "prometheus", + "firstObserved": "2026-03-26T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z" + }, + { + "id": "ana-011", + "configID": "cfg-eks-001", + "analyzer": "Trivy", + "message": "Resolved: CVE-2025-9999 in nginx ingress controller patched in v1.10.1", + "status": "resolved", + "severity": "high", + "analysisType": "security", + "source": "trivy-operator", + "firstObserved": "2026-02-01T09:00:00Z", + "lastObserved": "2026-03-15T09:00:00Z" + } + ], + "relationships": [ + { + "configID": "cfg-eks-001", + "relatedID": "cfg-vpc-001", + "relation": "RunsIn", + "direction": "outgoing" + }, + { + "configID": "cfg-eks-001", + "relatedID": "cfg-iam-001", + "relation": "ManagedBy", + "direction": "outgoing" + }, + { + "configID": "cfg-eks-001", + "relatedID": "cfg-sg-001", + "relation": "DependsOn", + "direction": "outgoing" + }, + { + "configID": "cfg-eks-001", + "relatedID": "cfg-rds-001", + "relation": "DependsOn", + "direction": "outgoing" + }, + { + "configID": "cfg-deploy-001", + "relatedID": "cfg-eks-001", + "relation": "RunsOn", + "direction": "incoming" + }, + { + "configID": "cfg-deploy-002", + "relatedID": "cfg-eks-001", + "relation": "RunsOn", + "direction": "incoming" + }, + { + "configID": "cfg-deploy-003", + "relatedID": "cfg-eks-001", + "relation": "RunsOn", + "direction": "incoming" + }, + { + "configID": "cfg-ns-001", + "relatedID": "cfg-eks-001", + "relation": "ChildOf", + "direction": "incoming" + }, + { + "configID": "cfg-node-001", + "relatedID": "cfg-eks-001", + "relation": "ChildOf", + "direction": "incoming" + }, + { + "configID": "cfg-node-002", + "relatedID": "cfg-eks-001", + "relation": "ChildOf", + "direction": "incoming" + } + ], + "relatedConfigs": [ + { + "id": "cfg-vpc-001", + "name": "prod-vpc", + "type": "AWS::EC2::VPC", + "configClass": "Network", + "status": "available", + "health": "healthy", + "labels": { + "env": "production" + } + }, + { + "id": "cfg-iam-001", + "name": "eks-cluster-role", + "type": "AWS::IAM::Role", + "configClass": "IAM", + "status": "active", + "health": "healthy" + }, + { + "id": "cfg-sg-001", + "name": "eks-cluster-sg", + "type": "AWS::EC2::SecurityGroup", + "configClass": "Network", + "status": "active", + "health": "warning", + "labels": { + "env": "production" + } + }, + { + "id": "cfg-rds-001", + "name": "mission-control-db", + "type": "AWS::RDS::Instance", + "configClass": "Database", + "status": "available", + "health": "healthy", + "labels": { + "env": "production", + "engine": "postgresql" + } + }, + { + "id": "cfg-deploy-001", + "name": "incident-commander", + "type": "Kubernetes::Deployment", + "configClass": "Deployment", + "status": "Running", + "health": "healthy", + "labels": { + "app": "incident-commander" + } + }, + { + "id": "cfg-deploy-002", + "name": "canary-checker", + "type": "Kubernetes::Deployment", + "configClass": "Deployment", + "status": "Running", + "health": "healthy", + "labels": { + "app": "canary-checker" + } + }, + { + "id": "cfg-deploy-003", + "name": "config-db", + "type": "Kubernetes::Deployment", + "configClass": "Deployment", + "status": "Running", + "health": "unhealthy", + "labels": { + "app": "config-db" + } + }, + { + "id": "cfg-ns-001", + "name": "mc", + "type": "Kubernetes::Namespace", + "configClass": "Namespace", + "status": "Active", + "health": "healthy" + }, + { + "id": "cfg-node-001", + "name": "ip-10-0-1-42", + "type": "Kubernetes::Node", + "configClass": "Node", + "status": "Ready", + "health": "healthy" + }, + { + "id": "cfg-node-002", + "name": "ip-10-0-2-18", + "type": "Kubernetes::Node", + "configClass": "Node", + "status": "Ready", + "health": "warning", + "labels": { + "instance-type": "i3.xlarge" + } + } + ], + "rbacChanges": [ + { + "id": "rbac-001", + "date": "2026-03-30T09:12:00Z", + "changeType": "PermissionAdded", + "source": "azure-entra", + "createdBy": "alice@flanksource.com", + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "permission": { + "user": "alice@flanksource.com", + "role": "db_owner", + "group": "incident-responders" + }, + "description": "PermissionAdded: user alice@flanksource.com, role db_owner, group incident-responders", + "status": "info", + "createdAt": "2026-03-30T09:12:00Z" + }, + { + "id": "rbac-002", + "date": "2026-03-29T18:40:00Z", + "changeType": "PermissionRemoved", + "source": "azure-entra", + "createdBy": "security-automation", + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "permission": { + "user": "contractor-temp", + "role": "db_datareader" + }, + "description": "PermissionRemoved: user contractor-temp, role db_datareader", + "status": "info", + "createdAt": "2026-03-29T18:40:00Z" + }, + { + "id": "rbac-006", + "date": "2026-03-29T11:15:00Z", + "changeType": "PermissionAdded", + "source": "okta", + "createdBy": "governance-bot", + "configId": "cfg-keyvault-001", + "configName": "prod-keyvault", + "configType": "Azure::KeyVault", + "permission": { + "user": "ops-auditor", + "role": "Secrets Reader" + }, + "description": "PermissionAdded: user ops-auditor, role Secrets Reader", + "status": "info", + "createdAt": "2026-03-29T11:15:00Z" + }, + { + "id": "rbac-003", + "date": "2026-03-29T16:00:00Z", + "changeType": "AccessReviewed", + "source": "access-review-job", + "createdBy": "governance-bot", + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "description": "Quarterly review completed for production database roles", + "status": "info", + "createdAt": "2026-03-29T16:00:00Z" + }, + { + "id": "rbac-004", + "date": "2026-03-28T13:05:00Z", + "changeType": "PermissionGranted", + "source": "okta", + "createdBy": "bob@flanksource.com", + "configId": "cfg-analytics-001", + "configName": "analytics-db", + "configType": "MSSQL::Database", + "description": "Granted db_ddladmin to deploy-bot on analytics-db", + "status": "info", + "createdAt": "2026-03-28T13:05:00Z" + }, + { + "id": "rbac-005", + "date": "2026-03-27T07:20:00Z", + "changeType": "PermissionAdded", + "source": "okta", + "configId": "cfg-keyvault-001", + "configName": "prod-keyvault", + "configType": "Azure::KeyVault", + "permission": { + "group": "break-glass-admins", + "role": "Secrets Officer" + }, + "description": "PermissionAdded: role Secrets Officer, group break-glass-admins", + "status": "info", + "createdAt": "2026-03-27T07:20:00Z" + } + ], + "backupChanges": [ + { + "id": "bak-001", + "date": "2026-03-30T02:00:00Z", + "changeType": "BackupStarted", + "source": "aws-backup", + "description": "Nightly snapshot started for incident-commander-db", + "status": "info", + "createdAt": "2026-03-30T02:00:00Z" + }, + { + "id": "bak-002", + "date": "2026-03-30T02:08:00Z", + "changeType": "BackupCompleted", + "source": "aws-backup", + "description": "Nightly snapshot completed for incident-commander-db (4.3 GB)", + "status": "info", + "createdAt": "2026-03-30T02:08:00Z" + }, + { + "id": "bak-003", + "date": "2026-03-29T02:01:00Z", + "changeType": "BackupFailed", + "source": "aws-backup", + "description": "Snapshot failed for incident-commander-db after storage timeout", + "status": "high", + "createdAt": "2026-03-29T02:01:00Z" + }, + { + "id": "bak-004", + "date": "2026-03-28T12:10:00Z", + "changeType": "BackupRestored", + "source": "drill-playbook", + "createdBy": "platform-oncall", + "description": "Restored staging copy from nightly snapshot for disaster recovery drill", + "status": "info", + "createdAt": "2026-03-28T12:10:00Z" + }, + { + "id": "bak-005", + "date": "2026-03-28T12:18:00Z", + "changeType": "RestoreCompleted", + "source": "drill-playbook", + "createdBy": "platform-oncall", + "description": "Restore completed and validation checks passed", + "status": "info", + "createdAt": "2026-03-28T12:18:00Z" + }, + { + "id": "bak-006", + "date": "2026-03-27T02:00:00Z", + "changeType": "BackupEnqueued", + "source": "aws-backup", + "description": "Queued backup job for archive-postgres", + "status": "info", + "createdAt": "2026-03-27T02:00:00Z" + }, + { + "id": "bak-007", + "date": "2026-03-27T10:00:00Z", + "changeType": "diff", + "source": "terraform", + "description": "This diff should be filtered out of the backup-focused renderer", + "status": "low", + "createdAt": "2026-03-27T10:00:00Z" + } + ], + "deploymentChanges": [ + { + "id": "dep-001", + "date": "2026-03-30T08:15:00Z", + "changeType": "diff", + "source": "argocd", + "createdBy": "deploy-bot", + "description": "Deployment incident-commander image updated: v1.4.199 -> v1.4.200", + "status": "low", + "createdAt": "2026-03-30T08:15:00Z" + }, + { + "id": "dep-002", + "date": "2026-03-30T07:30:00Z", + "changeType": "Pulled", + "source": "kubernetes", + "description": "Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42", + "status": "info", + "createdAt": "2026-03-30T07:30:00Z" + }, + { + "id": "dep-003", + "date": "2026-03-29T22:00:00Z", + "changeType": "ScalingReplicaSet", + "source": "kubernetes", + "createdBy": "cluster-autoscaler", + "description": "Deployment incident-commander scaled from 2 to 4 replicas", + "status": "low", + "createdAt": "2026-03-29T22:00:00Z" + }, + { + "id": "dep-004", + "date": "2026-03-29T14:00:00Z", + "changeType": "PolicyUpdate", + "source": "argocd", + "createdBy": "alice@flanksource.com", + "description": "Deployment network policy updated to restrict egress to approved CIDRs", + "status": "medium", + "createdAt": "2026-03-29T14:00:00Z" + }, + { + "id": "dep-005", + "date": "2026-03-28T10:00:00Z", + "changeType": "FieldsV1", + "source": "kubernetes", + "description": "FieldsV1 payload updated during reconciliation", + "status": "info", + "createdAt": "2026-03-28T10:00:00Z" + }, + { + "id": "dep-006", + "date": "2026-03-27T09:00:00Z", + "changeType": "diff", + "source": "terraform", + "createdBy": "carol@flanksource.com", + "description": "Deployment incident-commander rollout template updated with new topology spread constraints", + "status": "medium", + "createdAt": "2026-03-27T09:00:00Z" + } + ], + "scrapers": [ + { + "id": "scr-001", + "name": "mc/aws-production", + "namespace": "mc", + "source": "KubernetesCRD", + "types": [ + "aws", + "kubernetes" + ], + "specHash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd", + "createdBy": "alice@flanksource.com", + "createdAt": "2025-06-10T09:00:00Z", + "updatedAt": "2026-03-28T14:30:00Z", + "gitops": { + "git": { + "url": "https://github.com/flanksource/mission-control-demo", + "branch": "main", + "file": "clusters/prod/scrapers/aws.yaml", + "dir": "clusters/prod/scrapers", + "link": "https://github.com/flanksource/mission-control-demo/tree/main/clusters/prod/scrapers/aws.yaml" + }, + "kustomize": { + "path": "clusters/prod/scrapers", + "file": "clusters/prod/scrapers/kustomization.yaml" + } + } + }, + { + "id": "scr-002", + "name": "mc/azure-entra", + "namespace": "mc", + "source": "KubernetesCRD", + "types": [ + "azure" + ], + "specHash": "ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33dd44ee55ff00aa11bb22cc33", + "createdAt": "2025-09-01T10:00:00Z", + "updatedAt": "2026-03-30T08:00:00Z" + }, + { + "id": "scr-003", + "name": "local-file-scraper", + "source": "ConfigFile", + "types": [ + "file", + "sql" + ], + "specHash": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "createdBy": "bob@flanksource.com", + "createdAt": "2026-01-15T11:00:00Z" + } + ], + "genericChangesSection": { + "type": "changes", + "title": "Recent Infrastructure Changes", + "changes": [ + { + "id": "gen-001", + "date": "2026-03-30T10:00:00Z", + "changeType": "ConfigUpdate", + "source": "terraform", + "createdBy": "alice@flanksource.com", + "description": "Updated VPC CIDR block from 10.0.0.0/16 to 10.0.0.0/12", + "status": "medium", + "createdAt": "2026-03-30T10:00:00Z" + }, + { + "id": "gen-002", + "date": "2026-03-29T15:00:00Z", + "changeType": "TagUpdate", + "source": "aws-config", + "description": "Added cost-center tag to 14 resources in us-east-1", + "status": "info", + "createdAt": "2026-03-29T15:00:00Z" + }, + { + "id": "gen-003", + "date": "2026-03-28T09:00:00Z", + "changeType": "SecurityGroupChange", + "source": "aws-config", + "createdBy": "security-automation", + "description": "Removed unused ingress rule on sg-0abc123 (port 8080)", + "status": "low", + "createdAt": "2026-03-28T09:00:00Z" + }, + { + "id": "gen-004", + "date": "2026-03-27T14:00:00Z", + "changeType": "DNSUpdate", + "source": "route53", + "createdBy": "bob@flanksource.com", + "description": "Added CNAME record api-v2.flanksource.com -> prod-alb.us-east-1.elb.amazonaws.com", + "status": "info", + "createdAt": "2026-03-27T14:00:00Z" + } + ] + }, + "dynamicViewSection": { + "type": "view", + "title": "Cluster Nodes", + "view": { + "columns": [ + { + "name": "node", + "type": "string" + }, + { + "name": "status", + "type": "status" + }, + { + "name": "health", + "type": "health" + }, + { + "name": "cpu_used", + "type": "gauge", + "gauge": { + "thresholds": [ + { + "percent": 0, + "color": "#22C55E" + }, + { + "percent": 70, + "color": "#EAB308" + }, + { + "percent": 90, + "color": "#EF4444" + } + ] + } + }, + { + "name": "memory", + "type": "bytes" + } + ], + "rows": [ + [ + "ip-10-0-1-42", + "Ready", + "healthy", + 45.2, + 8589934592 + ], + [ + "ip-10-0-2-18", + "Ready", + "warning", + 87.3, + 14495514624 + ], + [ + "ip-10-0-3-7", + "NotReady", + "unhealthy", + 0, + 0 + ] + ] + } + }, + "dynamicConfigsSection": { + "type": "configs", + "title": "Related Databases", + "configs": [ + { + "id": "db-001", + "name": "mission-control-db", + "type": "AWS::RDS::Instance", + "status": "available", + "health": "healthy", + "labels": { + "engine": "postgresql", + "env": "production" + } + }, + { + "id": "db-002", + "name": "analytics-db", + "type": "AWS::RDS::Instance", + "status": "available", + "health": "warning", + "labels": { + "engine": "postgresql", + "env": "production" + } + }, + { + "id": "db-003", + "name": "archive-db", + "type": "AWS::RDS::Instance", + "status": "stopped", + "health": "unknown", + "labels": { + "engine": "postgresql", + "env": "staging" + } + } + ] + }, + "application": { + "id": "app-001", + "name": "Mission Control", + "type": "WebApplication", + "namespace": "mc", + "description": "Internal developer platform for Kubernetes fleet management", + "properties": [ + { + "name": "uptime", + "label": "Uptime", + "value": 99.97, + "unit": "percentage", + "order": 1 + }, + { + "name": "latency", + "label": "P99 Latency", + "value": 245, + "unit": "milliseconds", + "order": 2 + }, + { + "name": "requests", + "label": "Requests/s", + "value": 1240, + "order": 3 + }, + { + "name": "error_rate", + "label": "Error Rate", + "value": 0.03, + "unit": "percentage", + "order": 4 + } + ], + "accessControl": { + "users": [ + { + "id": "u-001", + "name": "Alice Johnson", + "email": "alice@flanksource.com", + "role": "admin", + "authType": "SSO", + "created": "2025-01-15T09:00:00Z", + "lastLogin": "2026-03-30T08:00:00Z", + "lastAccessReview": "2026-03-15T10:00:00Z" + }, + { + "id": "u-002", + "name": "Bob Smith", + "email": "bob@flanksource.com", + "role": "editor", + "authType": "SSO", + "created": "2025-06-01T09:00:00Z", + "lastLogin": "2026-03-28T14:00:00Z", + "lastAccessReview": "2026-02-01T10:00:00Z" + }, + { + "id": "u-003", + "name": "Carol Davis", + "email": "carol@flanksource.com", + "role": "viewer", + "authType": "API Key", + "created": "2026-01-10T09:00:00Z", + "lastLogin": null, + "lastAccessReview": null + } + ], + "authentication": [ + { + "name": "Azure AD SSO", + "type": "SAML", + "mfa": { + "type": "TOTP", + "enforced": "true" + }, + "properties": { + "tenant": "flanksource.onmicrosoft.com" + } + }, + { + "name": "API Key Auth", + "type": "Bearer", + "mfa": { + "type": "none", + "enforced": "false" + }, + "properties": { + "rotation": "90 days" + } + } + ] + }, + "incidents": [ + { + "id": "inc-001", + "date": "2026-03-28T03:15:00Z", + "severity": "critical", + "description": "Database connection pool exhausted causing 503 errors across all API endpoints", + "status": "resolved", + "resolvedDate": "2026-03-28T04:45:00Z" + }, + { + "id": "inc-002", + "date": "2026-03-25T14:00:00Z", + "severity": "high", + "description": "Config scraper failing to sync AWS resources due to expired IAM credentials", + "status": "resolved", + "resolvedDate": "2026-03-25T15:30:00Z" + }, + { + "id": "inc-003", + "date": "2026-03-22T09:30:00Z", + "severity": "medium", + "description": "Notification delivery delayed by 15+ minutes due to queue backlog", + "status": "resolved", + "resolvedDate": "2026-03-22T11:00:00Z" + }, + { + "id": "inc-004", + "date": "2026-03-30T06:00:00Z", + "severity": "low", + "description": "Health check dashboard showing stale data for canary-checker pods", + "status": "open" + }, + { + "id": "inc-005", + "date": "2026-03-29T20:00:00Z", + "severity": "high", + "description": "Memory leak in event processor causing gradual degradation", + "status": "open" + } + ], + "locations": [ + { + "account": "flanksource-prod", + "name": "us-east-1-primary", + "type": "EKS Cluster", + "purpose": "primary", + "region": "us-east-1", + "provider": "AWS", + "resourceCount": 142 + }, + { + "account": "flanksource-prod", + "name": "eu-west-1-backup", + "type": "EKS Cluster", + "purpose": "backup", + "region": "eu-west-1", + "provider": "AWS", + "resourceCount": 38 + }, + { + "account": "flanksource-dr", + "name": "us-west-2-dr", + "type": "EKS Cluster", + "purpose": "dr", + "region": "us-west-2", + "provider": "AWS", + "resourceCount": 15 + } + ], + "backups": [ + { + "id": "bkp-001", + "database": "mission-control-db", + "type": "snapshot", + "source": "aws-backup", + "date": "2026-03-30T02:00:00Z", + "size": "4.3 GB", + "status": "success" + }, + { + "id": "bkp-002", + "database": "mission-control-db", + "type": "snapshot", + "source": "aws-backup", + "date": "2026-03-29T02:00:00Z", + "size": "4.2 GB", + "status": "failed" + }, + { + "id": "bkp-003", + "database": "mission-control-db", + "type": "snapshot", + "source": "aws-backup", + "date": "2026-03-28T02:00:00Z", + "size": "4.1 GB", + "status": "success" + }, + { + "id": "bkp-004", + "database": "analytics-db", + "type": "logical", + "source": "pg_dump", + "date": "2026-03-30T03:00:00Z", + "size": "1.8 GB", + "status": "success" + }, + { + "id": "bkp-005", + "database": "analytics-db", + "type": "logical", + "source": "pg_dump", + "date": "2026-03-29T03:00:00Z", + "size": "1.7 GB", + "status": "in-progress" + } + ], + "restores": [ + { + "id": "rst-001", + "database": "mission-control-db", + "date": "2026-03-28T12:00:00Z", + "source": "aws-backup", + "status": "success", + "completedAt": "2026-03-28T12:18:00Z" + }, + { + "id": "rst-002", + "database": "analytics-db", + "date": "2026-03-15T09:00:00Z", + "source": "pg_dump", + "status": "success", + "completedAt": "2026-03-15T09:35:00Z" + } + ], + "findings": [ + { + "id": "find-001", + "type": "security", + "severity": "critical", + "title": "CVE-2026-0891 in libcrypto", + "description": "Critical vulnerability in OpenSSL library", + "date": "2026-03-25T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "open", + "remediation": "Upgrade base image to golang:1.23.1-alpine" + }, + { + "id": "find-002", + "type": "security", + "severity": "high", + "title": "Container running as root", + "description": "incident-commander pod runs as UID 0", + "date": "2026-03-20T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "open", + "remediation": "Add securityContext.runAsNonRoot to deployment spec" + }, + { + "id": "find-003", + "type": "compliance", + "severity": "medium", + "title": "Missing data-classification label", + "description": "Namespace mc missing required label", + "date": "2026-03-15T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "open", + "remediation": "Add label data-classification=internal to namespace" + }, + { + "id": "find-004", + "type": "compliance", + "severity": "low", + "title": "Pod disruption budget missing", + "description": "canary-checker deployment has no PDB", + "date": "2026-03-10T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "accepted" + }, + { + "id": "find-005", + "type": "reliability", + "severity": "high", + "title": "Node memory pressure", + "description": "ip-10-0-2-18 consistently above 85% memory", + "date": "2026-03-26T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "open", + "remediation": "Scale node group or add memory limits to workloads" + }, + { + "id": "find-006", + "type": "reliability", + "severity": "medium", + "title": "Single replica deployment", + "description": "config-db running with 1 replica", + "date": "2026-03-01T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "resolved" + }, + { + "id": "find-007", + "type": "performance", + "severity": "high", + "title": "API latency above threshold", + "description": "P99 latency exceeded 500ms 12 times in 7 days", + "date": "2026-03-23T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "in-progress", + "remediation": "Investigate slow queries and add connection pooling" + }, + { + "id": "find-008", + "type": "performance", + "severity": "low", + "title": "Slow config scraper sync", + "description": "AWS scraper taking >10min per cycle", + "date": "2026-03-18T09:00:00Z", + "lastObserved": "2026-03-30T09:00:00Z", + "status": "open" + } + ], + "sections": [] + }, + "rbacReport": { + "title": "prod-sql-primary", + "query": "type=MSSQL::Database AND name=prod-sql-primary", + "generatedAt": "2026-03-30T12:00:00Z", + "subject": { + "id": "cfg-sql-001", + "name": "prod-sql-primary", + "type": "MSSQL::Database", + "config_class": "Database", + "status": "Online", + "health": "healthy", + "description": "Primary SQL Server database for production workloads", + "tags": { + "env": "production", + "team": "data-platform" + } + }, + "parents": [ + { + "id": "cfg-sql-server-001", + "name": "sql-prod-east", + "type": "MSSQL::Server" + }, + { + "id": "cfg-rg-001", + "name": "rg-prod-data", + "type": "Azure::ResourceGroup" + } + ], + "summary": { + "totalUsers": 8, + "totalResources": 2, + "staleAccessCount": 2, + "overdueReviews": 1, + "directAssignments": 6, + "groupAssignments": 4 + }, + "resources": [ + { + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "configClass": "Database", + "path": "rg-prod-data.sql-prod-east.prod-sql-primary", + "status": "Online", + "health": "healthy", + "tags": { + "env": "production" + }, + "labels": { + "team": "data-platform" + }, + "users": [ + { + "userId": "u-alice", + "userName": "alice@flanksource.com", + "email": "alice@flanksource.com", + "role": "db_owner", + "roleSource": "direct", + "sourceSystem": "azure-entra", + "createdAt": "2025-01-15T09:00:00Z", + "lastSignedInAt": "2026-03-30T08:00:00Z", + "lastReviewedAt": "2026-03-15T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "userId": "u-bob", + "userName": "bob@flanksource.com", + "email": "bob@flanksource.com", + "role": "db_datareader", + "roleSource": "direct", + "sourceSystem": "azure-entra", + "createdAt": "2025-06-01T09:00:00Z", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "lastReviewedAt": "2026-02-01T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "userId": "u-bob", + "userName": "bob@flanksource.com", + "email": "bob@flanksource.com", + "role": "db_datawriter", + "roleSource": "group:SG-DataEngineers", + "sourceSystem": "azure-entra", + "createdAt": "2025-06-01T09:00:00Z", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "lastReviewedAt": "2026-02-01T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "userId": "u-carol", + "userName": "carol@flanksource.com", + "email": "carol@flanksource.com", + "role": "db_datareader", + "roleSource": "group:SG-Analytics", + "sourceSystem": "azure-entra", + "createdAt": "2026-01-10T09:00:00Z", + "lastSignedInAt": null, + "lastReviewedAt": null, + "isStale": true, + "isReviewOverdue": true + }, + { + "userId": "u-deploy-bot", + "userName": "deploy-bot", + "email": "deploy-bot@flanksource.com", + "role": "db_ddladmin", + "roleSource": "direct", + "sourceSystem": "azure-entra", + "createdAt": "2025-08-01T09:00:00Z", + "lastSignedInAt": "2026-03-30T06:00:00Z", + "lastReviewedAt": "2026-03-01T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "userId": "u-contractor", + "userName": "contractor-temp", + "email": "contractor@external.com", + "role": "db_datareader", + "roleSource": "direct", + "sourceSystem": "okta", + "createdAt": "2025-12-01T09:00:00Z", + "lastSignedInAt": "2025-12-15T10:00:00Z", + "lastReviewedAt": null, + "isStale": true, + "isReviewOverdue": true + } + ], + "changelog": [ + { + "configId": "cfg-sql-001", + "date": "2026-03-30T09:12:00Z", + "changeType": "PermissionGranted", + "user": "alice@flanksource.com", + "role": "db_owner", + "configName": "prod-sql-primary", + "source": "azure-entra", + "description": "Granted during oncall rotation" + }, + { + "configId": "cfg-sql-001", + "date": "2026-03-29T18:40:00Z", + "changeType": "PermissionRevoked", + "user": "contractor-temp", + "role": "db_datareader", + "configName": "prod-sql-primary", + "source": "azure-entra", + "description": "Contract ended" + } + ] + } + ], + "changelog": [ + { + "configId": "cfg-sql-001", + "date": "2026-03-30T09:12:00Z", + "changeType": "PermissionGranted", + "user": "alice@flanksource.com", + "role": "db_owner", + "configName": "prod-sql-primary", + "source": "azure-entra", + "description": "Granted during oncall rotation" + }, + { + "configId": "cfg-sql-001", + "date": "2026-03-29T18:40:00Z", + "changeType": "PermissionRevoked", + "user": "contractor-temp", + "role": "db_datareader", + "configName": "prod-sql-primary", + "source": "azure-entra", + "description": "Contract ended" + }, + { + "configId": "cfg-sql-001", + "date": "2026-03-29T16:00:00Z", + "changeType": "AccessReviewed", + "user": "governance-bot", + "role": "all", + "configName": "prod-sql-primary", + "source": "access-review-job", + "description": "Quarterly review completed" + }, + { + "configId": "cfg-sql-001", + "date": "2026-03-28T13:05:00Z", + "changeType": "PermissionGranted", + "user": "deploy-bot", + "role": "db_ddladmin", + "configName": "prod-sql-primary", + "source": "azure-entra", + "description": "Automated pipeline access" + }, + { + "configId": "cfg-sql-001", + "date": "2026-03-15T10:00:00Z", + "changeType": "PermissionGranted", + "user": "bob@flanksource.com", + "role": "db_datawriter", + "configName": "prod-sql-primary", + "source": "azure-entra", + "description": "Added via SG-DataEngineers group" + }, + { + "configId": "cfg-sql-001", + "date": "2026-03-01T09:00:00Z", + "changeType": "PermissionRevoked", + "user": "intern-2025", + "role": "db_datareader", + "configName": "prod-sql-primary", + "source": "okta", + "description": "Internship ended" + } + ], + "users": [ + { + "userId": "u-alice", + "userName": "alice@flanksource.com", + "email": "alice@flanksource.com", + "sourceSystem": "azure-entra", + "lastSignedInAt": "2026-03-30T08:00:00Z", + "resources": [ + { + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "configClass": "Database", + "role": "db_owner", + "roleSource": "direct", + "createdAt": "2025-01-15T09:00:00Z", + "lastSignedInAt": "2026-03-30T08:00:00Z", + "lastReviewedAt": "2026-03-15T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "configId": "cfg-sql-002", + "configName": "analytics-db", + "configType": "MSSQL::Database", + "configClass": "Database", + "role": "db_datareader", + "roleSource": "group:SG-Analytics", + "createdAt": "2025-03-01T09:00:00Z", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "lastReviewedAt": "2026-03-15T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + } + ] + }, + { + "userId": "u-bob", + "userName": "bob@flanksource.com", + "email": "bob@flanksource.com", + "sourceSystem": "azure-entra", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "resources": [ + { + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "configClass": "Database", + "role": "db_datareader", + "roleSource": "direct", + "createdAt": "2025-06-01T09:00:00Z", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "lastReviewedAt": "2026-02-01T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "configClass": "Database", + "role": "db_datawriter", + "roleSource": "group:SG-DataEngineers", + "createdAt": "2025-06-01T09:00:00Z", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "lastReviewedAt": "2026-02-01T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + }, + { + "configId": "cfg-kv-001", + "configName": "prod-keyvault", + "configType": "Azure::KeyVault", + "configClass": "Security", + "role": "Secrets Reader", + "roleSource": "direct", + "createdAt": "2025-09-01T09:00:00Z", + "lastSignedInAt": "2026-03-25T10:00:00Z", + "lastReviewedAt": "2026-01-15T10:00:00Z", + "isStale": false, + "isReviewOverdue": false + } + ] + } + ] + }, + "catalogReport": { + "relationshipTree": { + "id": "cfg-eks-001", + "name": "prod-eks-cluster", + "type": "AWS::EKS::Cluster", + "edgeType": "target", + "children": [ + { + "id": "cfg-vpc-001", + "name": "prod-vpc", + "type": "AWS::EC2::VPC", + "edgeType": "parent", + "relation": "RunsIn", + "children": [ + { + "id": "cfg-subnet-001", + "name": "private-subnet-1a", + "type": "AWS::EC2::Subnet", + "edgeType": "child" + }, + { + "id": "cfg-subnet-002", + "name": "private-subnet-1b", + "type": "AWS::EC2::Subnet", + "edgeType": "child" + } + ] + }, + { + "id": "cfg-ns-001", + "name": "mc", + "type": "Kubernetes::Namespace", + "edgeType": "child", + "relation": "ChildOf", + "children": [ + { + "id": "cfg-deploy-001", + "name": "incident-commander", + "type": "Kubernetes::Deployment", + "edgeType": "child" + }, + { + "id": "cfg-deploy-002", + "name": "canary-checker", + "type": "Kubernetes::Deployment", + "edgeType": "child" + } + ] + }, + { + "id": "cfg-rds-001", + "name": "mission-control-db", + "type": "AWS::RDS::Instance", + "edgeType": "related", + "relation": "DependsOn" + } + ] + }, + "access": [ + { + "userId": "u-alice", + "userName": "Alice Johnson", + "email": "alice@flanksource.com", + "role": "admin", + "userType": "User", + "createdAt": "2025-01-15T09:00:00Z", + "lastSignedInAt": "2026-03-30T08:00:00Z", + "lastReviewedAt": "2026-03-15T10:00:00Z" + }, + { + "userId": "u-bob", + "userName": "Bob Smith", + "email": "bob@flanksource.com", + "role": "editor", + "userType": "User", + "createdAt": "2025-06-01T09:00:00Z", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "lastReviewedAt": "2026-02-01T10:00:00Z" + }, + { + "userId": "u-carol", + "userName": "Carol Davis", + "email": "carol@flanksource.com", + "role": "viewer", + "userType": "User", + "createdAt": "2026-01-10T09:00:00Z", + "lastSignedInAt": null + }, + { + "userId": "u-deploy-bot", + "userName": "deploy-bot", + "email": "deploy-bot@flanksource.com", + "role": "editor", + "userType": "ServiceAccount", + "createdAt": "2025-08-01T09:00:00Z", + "lastSignedInAt": "2026-03-30T06:00:00Z", + "lastReviewedAt": "2026-03-01T10:00:00Z" + }, + { + "userId": "u-stale", + "userName": "Former Employee", + "email": "former@flanksource.com", + "role": "viewer", + "userType": "User", + "createdAt": "2024-06-01T09:00:00Z", + "lastSignedInAt": "2025-06-15T10:00:00Z" + } + ], + "accessLogs": [ + { + "userId": "u-alice", + "userName": "Alice Johnson", + "configName": "prod-eks-cluster", + "configType": "AWS::EKS::Cluster", + "createdAt": "2026-03-30T08:15:00Z", + "mfa": true, + "count": 3, + "properties": { + "action": "describe-cluster", + "source": "kubectl" + } + }, + { + "userId": "u-bob", + "userName": "Bob Smith", + "configName": "prod-eks-cluster", + "configType": "AWS::EKS::Cluster", + "createdAt": "2026-03-29T14:30:00Z", + "mfa": true, + "count": 1 + }, + { + "userId": "u-deploy-bot", + "userName": "deploy-bot", + "configName": "prod-eks-cluster", + "configType": "AWS::EKS::Cluster", + "createdAt": "2026-03-30T06:00:00Z", + "mfa": false, + "count": 12, + "properties": { + "action": "apply", + "source": "argocd" + } + }, + { + "userId": "u-carol", + "userName": "Carol Davis", + "configName": "prod-eks-cluster", + "configType": "AWS::EKS::Cluster", + "createdAt": "2026-03-25T11:00:00Z", + "mfa": false, + "count": 1 + }, + { + "userId": "u-alice", + "userName": "Alice Johnson", + "configName": "mission-control-db", + "configType": "AWS::RDS::Instance", + "createdAt": "2026-03-30T09:00:00Z", + "mfa": true, + "count": 2, + "properties": { + "action": "connect", + "source": "psql" + } + }, + { + "userId": "u-stale", + "userName": "Former Employee", + "configName": "prod-eks-cluster", + "configType": "AWS::EKS::Cluster", + "createdAt": "2025-06-15T10:00:00Z", + "mfa": false, + "count": 1 + } + ], + "entries": [ + { + "configItem": { + "id": "cfg-deploy-001", + "name": "incident-commander", + "type": "Kubernetes::Deployment", + "status": "Running", + "health": "healthy" + }, + "changeCount": 5, + "insightCount": 3, + "accessCount": 2, + "changes": [], + "analyses": [], + "access": [], + "accessLogs": [] + }, + { + "configItem": { + "id": "cfg-deploy-002", + "name": "canary-checker", + "type": "Kubernetes::Deployment", + "status": "Running", + "health": "healthy" + }, + "changeCount": 2, + "insightCount": 1, + "accessCount": 1, + "changes": [], + "analyses": [], + "access": [], + "accessLogs": [] + }, + { + "configItem": { + "id": "cfg-deploy-003", + "name": "config-db", + "type": "Kubernetes::Deployment", + "status": "Running", + "health": "unhealthy" + }, + "changeCount": 1, + "insightCount": 2, + "accessCount": 0, + "changes": [], + "analyses": [], + "access": [], + "accessLogs": [] + } + ], + "changes": [ + { + "id": "art-001", + "configID": "cfg-deploy-001", + "configName": "incident-commander", + "configType": "Kubernetes::Deployment", + "changeType": "diff", + "severity": "medium", + "source": "argocd", + "summary": "Deployment spec updated with new resource limits", + "createdAt": "2026-03-30T08:15:00Z", + "artifacts": [ + { + "id": "a-001", + "filename": "diff-screenshot.png", + "contentType": "image/png", + "size": 45000, + "dataUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + } + ] + }, + { + "id": "art-002", + "configID": "cfg-deploy-001", + "configName": "incident-commander", + "configType": "Kubernetes::Deployment", + "changeType": "PolicyUpdate", + "severity": "high", + "source": "argocd", + "summary": "Network policy tightened for egress", + "createdAt": "2026-03-29T14:00:00Z", + "artifacts": [ + { + "id": "a-002", + "filename": "policy-diff.yaml", + "contentType": "text/yaml", + "size": 1200 + } + ] + } + ], + "audit": { + "buildCommit": "3b3a1a0f", + "buildVersion": "v1.47.0", + "gitStatus": " M report/catalog/report.go\n M db/rbac.go", + "options": { + "title": "Production EKS Cluster Report", + "since": "720h", + "sections": { + "changes": true, + "insights": true, + "relationships": true, + "access": true, + "accessLogs": true, + "configJSON": false + }, + "recursive": true, + "groupBy": "type", + "changeArtifacts": true, + "thresholds": { + "staleDays": 90, + "reviewOverdueDays": 180 + }, + "filters": [ + "type=AWS::EKS::Cluster", + "namespace=mc" + ] + }, + "scrapers": [], + "queries": [ + { + "name": "RBACAccess", + "args": "configIDs=1 selectors=0", + "count": 42, + "duration": 128, + "pretty": "SELECT ... FROM config_access_summary WHERE config_id IN (?)" + }, + { + "name": "GroupMembers", + "args": "configIDs=1", + "count": 5, + "duration": 34, + "pretty": "SELECT ... FROM external_user_groups eug JOIN external_groups eg ..." + } + ], + "groups": [ + { + "id": "grp-admins", + "name": "mission-control-admins", + "groupType": "group", + "members": [ + { + "userId": "u-alice", + "name": "Alice Johnson", + "email": "alice@flanksource.com", + "userType": "user", + "lastSignedInAt": "2026-03-30T08:00:00Z", + "membershipAddedAt": "2025-01-10T09:00:00Z" + }, + { + "userId": "u-bob", + "name": "Bob Smith", + "email": "bob@flanksource.com", + "userType": "user", + "lastSignedInAt": "2026-03-28T14:00:00Z", + "membershipAddedAt": "2025-06-01T09:00:00Z" + }, + { + "userId": "u-stale", + "name": "Former Employee", + "email": "former@flanksource.com", + "userType": "user", + "lastSignedInAt": "2025-06-15T10:00:00Z", + "membershipAddedAt": "2024-06-01T09:00:00Z", + "membershipDeletedAt": "2025-07-01T09:00:00Z" + } + ] + }, + { + "id": "grp-readers", + "name": "mission-control-readers", + "groupType": "group", + "members": [ + { + "userId": "u-carol", + "name": "Carol Davis", + "email": "carol@flanksource.com", + "userType": "user", + "membershipAddedAt": "2026-01-10T09:00:00Z" + }, + { + "userId": "u-deploy-bot", + "name": "deploy-bot", + "email": "deploy-bot@flanksource.com", + "userType": "service_account", + "lastSignedInAt": "2026-03-30T06:00:00Z", + "membershipAddedAt": "2025-08-01T09:00:00Z" + } + ] + } + ] + } + }, + "viewReport": { + "name": "cluster-overview", + "title": "Cluster Overview", + "icon": "AWS::EKS::Cluster", + "columns": [ + { + "name": "name", + "type": "string" + }, + { + "name": "type", + "type": "string" + }, + { + "name": "health", + "type": "health" + }, + { + "name": "status", + "type": "status" + }, + { + "name": "cpu", + "type": "gauge", + "gauge": { + "thresholds": [ + { + "percent": 0, + "color": "#22C55E" + }, + { + "percent": 70, + "color": "#EAB308" + }, + { + "percent": 90, + "color": "#EF4444" + } + ] + } + }, + { + "name": "memory", + "type": "bytes" + }, + { + "name": "uptime", + "type": "duration" + }, + { + "name": "requests", + "type": "number", + "unit": "req/s" + }, + { + "name": "ready", + "type": "boolean" + }, + { + "name": "last_seen", + "type": "datetime" + }, + { + "name": "labels", + "type": "labels" + } + ], + "rows": [ + [ + "incident-commander", + "Deployment", + "healthy", + "Running", + 42.5, + 536870912, + 86400000000000, + 1240, + true, + "2026-03-30T08:00:00Z", + { + "app": "incident-commander", + "env": "production" + } + ], + [ + "canary-checker", + "Deployment", + "healthy", + "Running", + 18.3, + 268435456, + 172800000000000, + 450, + true, + "2026-03-30T08:00:00Z", + { + "app": "canary-checker", + "env": "production" + } + ], + [ + "config-db", + "Deployment", + "unhealthy", + "CrashLoopBackOff", + 92.1, + 1073741824, + 3600000000000, + 0, + false, + "2026-03-30T07:45:00Z", + { + "app": "config-db", + "env": "production" + } + ], + [ + "cert-manager", + "Deployment", + "healthy", + "Running", + 5.2, + 134217728, + 604800000000000, + 12, + true, + "2026-03-30T08:00:00Z", + { + "app": "cert-manager" + } + ], + [ + "nginx-ingress", + "Deployment", + "warning", + "Running", + 78.9, + 805306368, + 259200000000000, + 3200, + true, + "2026-03-30T08:00:00Z", + { + "app": "nginx-ingress", + "env": "production" + } + ] + ], + "panels": [ + { + "name": "Total Requests", + "type": "number", + "number": { + "unit": "req/s" + }, + "rows": [ + { + "value": 4902 + } + ] + }, + { + "name": "CPU Utilization", + "type": "gauge", + "gauge": { + "unit": "%", + "thresholds": [ + { + "percent": 0, + "color": "#22C55E" + }, + { + "percent": 70, + "color": "#EAB308" + }, + { + "percent": 90, + "color": "#EF4444" + } + ] + }, + "rows": [ + { + "value": 47.4 + } + ] + }, + { + "name": "Pod Distribution", + "type": "piechart", + "piechart": { + "showLabels": true, + "colors": { + "healthy": "#22C55E", + "warning": "#EAB308", + "unhealthy": "#EF4444" + } + }, + "rows": [ + { + "name": "healthy", + "value": 12 + }, + { + "name": "warning", + "value": 3 + }, + { + "name": "unhealthy", + "value": 1 + } + ] + }, + { + "name": "Memory by Service", + "type": "bargauge", + "bargauge": { + "unit": "bytes", + "max": 2147483648, + "thresholds": [ + { + "percent": 0, + "color": "#3B82F6" + }, + { + "percent": 70, + "color": "#EAB308" + }, + { + "percent": 90, + "color": "#EF4444" + } + ] + }, + "rows": [ + { + "name": "incident-commander", + "value": 536870912 + }, + { + "name": "config-db", + "value": 1073741824 + }, + { + "name": "nginx-ingress", + "value": 805306368 + }, + { + "name": "canary-checker", + "value": 268435456 + } + ] + }, + { + "name": "Cluster Status", + "type": "text", + "rows": [ + { + "value": "All critical services operational. config-db pod in CrashLoopBackOff - investigating OOM kills." + } + ] + }, + { + "name": "Recent Deployments", + "type": "table", + "rows": [ + { + "service": "incident-commander", + "version": "v1.4.200", + "deployed": "2026-03-30T08:15:00Z", + "status": "success" + }, + { + "service": "canary-checker", + "version": "v1.0.350", + "deployed": "2026-03-29T12:00:00Z", + "status": "success" + }, + { + "service": "config-db", + "version": "v2.1.0", + "deployed": "2026-03-30T07:30:00Z", + "status": "failed" + } + ] + } + ] + }, + "dynamicSections": [ + { + "type": "changes", + "title": "Permissions Added / Removed", + "changes": [ + { + "id": "rbac-001", + "date": "2026-03-30T09:12:00Z", + "changeType": "PermissionAdded", + "source": "azure-entra", + "createdBy": "alice@flanksource.com", + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "permission": { + "user": "alice@flanksource.com", + "role": "db_owner", + "group": "incident-responders" + }, + "description": "PermissionAdded: user alice@flanksource.com, role db_owner, group incident-responders", + "status": "info", + "createdAt": "2026-03-30T09:12:00Z" + }, + { + "id": "rbac-002", + "date": "2026-03-29T18:40:00Z", + "changeType": "PermissionRemoved", + "source": "azure-entra", + "createdBy": "security-automation", + "configId": "cfg-sql-001", + "configName": "prod-sql-primary", + "configType": "MSSQL::Database", + "permission": { + "user": "contractor-temp", + "role": "db_datareader" + }, + "description": "PermissionRemoved: user contractor-temp, role db_datareader", + "status": "info", + "createdAt": "2026-03-29T18:40:00Z" + }, + { + "id": "rbac-006", + "date": "2026-03-29T11:15:00Z", + "changeType": "PermissionAdded", + "source": "okta", + "createdBy": "governance-bot", + "configId": "cfg-keyvault-001", + "configName": "prod-keyvault", + "configType": "Azure::KeyVault", + "permission": { + "user": "ops-auditor", + "role": "Secrets Reader" + }, + "description": "PermissionAdded: user ops-auditor, role Secrets Reader", + "status": "info", + "createdAt": "2026-03-29T11:15:00Z" + }, + { + "id": "rbac-004", + "date": "2026-03-28T13:05:00Z", + "changeType": "PermissionGranted", + "source": "okta", + "createdBy": "bob@flanksource.com", + "configId": "cfg-analytics-001", + "configName": "analytics-db", + "configType": "MSSQL::Database", + "description": "Granted db_ddladmin to deploy-bot on analytics-db", + "status": "info", + "createdAt": "2026-03-28T13:05:00Z" + }, + { + "id": "rbac-005", + "date": "2026-03-27T07:20:00Z", + "changeType": "PermissionAdded", + "source": "okta", + "configId": "cfg-keyvault-001", + "configName": "prod-keyvault", + "configType": "Azure::KeyVault", + "permission": { + "group": "break-glass-admins", + "role": "Secrets Officer" + }, + "description": "PermissionAdded: role Secrets Officer, group break-glass-admins", + "status": "info", + "createdAt": "2026-03-27T07:20:00Z" + } + ] + }, + { + "type": "changes", + "title": "Backup Activity", + "changes": [ + { + "id": "bak-001", + "date": "2026-03-30T02:00:00Z", + "changeType": "BackupStarted", + "source": "aws-backup", + "description": "Nightly snapshot started for incident-commander-db", + "status": "info", + "createdAt": "2026-03-30T02:00:00Z" + }, + { + "id": "bak-002", + "date": "2026-03-30T02:08:00Z", + "changeType": "BackupCompleted", + "source": "aws-backup", + "description": "Nightly snapshot completed for incident-commander-db (4.3 GB)", + "status": "info", + "createdAt": "2026-03-30T02:08:00Z" + }, + { + "id": "bak-003", + "date": "2026-03-29T02:01:00Z", + "changeType": "BackupFailed", + "source": "aws-backup", + "description": "Snapshot failed for incident-commander-db after storage timeout", + "status": "high", + "createdAt": "2026-03-29T02:01:00Z" + } + ] + }, + { + "type": "changes", + "title": "Deployment Changes", + "changes": [ + { + "id": "dep-001", + "date": "2026-03-30T08:15:00Z", + "changeType": "diff", + "source": "argocd", + "createdBy": "deploy-bot", + "description": "Deployment incident-commander image updated: v1.4.199 -> v1.4.200", + "status": "low", + "createdAt": "2026-03-30T08:15:00Z" + }, + { + "id": "dep-002", + "date": "2026-03-30T07:30:00Z", + "changeType": "Pulled", + "source": "kubernetes", + "description": "Image flanksource/incident-commander:v1.4.200 pulled on node ip-10-0-1-42", + "status": "info", + "createdAt": "2026-03-30T07:30:00Z" + }, + { + "id": "dep-003", + "date": "2026-03-29T22:00:00Z", + "changeType": "ScalingReplicaSet", + "source": "kubernetes", + "createdBy": "cluster-autoscaler", + "description": "Deployment incident-commander scaled from 2 to 4 replicas", + "status": "low", + "createdAt": "2026-03-29T22:00:00Z" + } + ] + } + ] +} diff --git a/report/kitchen-sink/ChangesPage.tsx b/report/kitchen-sink/ChangesPage.tsx index bd14ef0a3..0176ba2ec 100644 --- a/report/kitchen-sink/ChangesPage.tsx +++ b/report/kitchen-sink/ChangesPage.tsx @@ -15,15 +15,27 @@ interface Props { } export default function ChangesPage({ data, pageProps }: Props) { + const allChanges = data.changes ?? []; + const schemaExampleChanges = allChanges.filter((change) => change.source === 'schema-examples'); + const demoChanges = allChanges.filter((change) => change.source !== 'schema-examples'); const rbacChanges = data.rbacChanges ?? []; const backupChanges = data.backupChanges ?? []; const deploymentChanges = data.deploymentChanges ?? []; const categoryMappings = (data as any).categoryMappings as CatalogReportCategoryMapping[] | undefined; - const categorized = categorizeChanges(data.changes ?? [], categoryMappings); + const categorized = categorizeChanges(demoChanges, categoryMappings); return ( - + + + {schemaExampleChanges.length > 0 && ( +
+
+ Full coverage for every standalone example in the duty handwritten change-types schema. Generated via make report/kitchen-sink.json and rendered once here in schema order. +
+ +
+ )}
@@ -49,7 +61,7 @@ export default function ChangesPage({ data, pageProps }: Props) { )}
- +
diff --git a/sdk/client_test.go b/sdk/client_test.go new file mode 100644 index 000000000..d7a03515e --- /dev/null +++ b/sdk/client_test.go @@ -0,0 +1,73 @@ +package sdk + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSDK(t *testing.T) { + RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "SDK") +} + +var _ = ginkgo.Describe("GetConnection HTML detection", func() { + ginkgo.It("returns ErrHTMLResponse when server returns HTML with 200 OK", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`Frontend`)) + })) + defer server.Close() + + client := New(server.URL, "fake-token") + _, err := client.GetConnection("any", "default") + Expect(errors.Is(err, ErrHTMLResponse)).To(BeTrue(), "got: %v", err) + }) + + ginkgo.It("returns ErrHTMLResponse when body starts with '<' even without HTML content-type", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`no content type header`)) + })) + defer server.Close() + + client := New(server.URL, "fake-token") + _, err := client.GetConnection("any", "default") + Expect(errors.Is(err, ErrHTMLResponse)).To(BeTrue(), "got: %v", err) + }) + + ginkgo.It("decodes JSON successfully when server returns valid JSON", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[{"name":"azure-bearer","namespace":"monitoring","type":"http"}]`)) + })) + defer server.Close() + + client := New(server.URL, "fake-token") + conn, err := client.GetConnection("azure-bearer", "monitoring") + Expect(err).ToNot(HaveOccurred()) + Expect(conn).ToNot(BeNil()) + Expect(conn.Name).To(Equal("azure-bearer")) + }) +}) + +var _ = ginkgo.Describe("TestConnection HTML detection", func() { + ginkgo.It("returns ErrHTMLResponse on HTML error page (405 from frontend proxy)", func() { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = w.Write([]byte(`405`)) + })) + defer server.Close() + + client := New(server.URL, "fake-token") + _, err := client.TestConnection("00000000-0000-0000-0000-000000000000") + Expect(errors.Is(err, ErrHTMLResponse)).To(BeTrue(), "got: %v", err) + }) +}) From 4ef8c54ea88c013a0fd600c28f02e0cf547311c7 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 14 Apr 2026 14:50:01 +0300 Subject: [PATCH 39/48] chore(build): update dependencies and add audit finding schemas Upgrade flanksource/duty to v1.0.1260, add charmbracelet TUI components and chromedp CDP protocol, clean up gitignore for test artifacts and tools --- .gitignore | 8 +- go.mod | 3 +- go.sum | 24 +- report/finding-schema.json | 438 ++++++++++++++++++++ report/sample-findings.json | 806 ++++++++++++++++++++++++++++++++++++ 5 files changed, 1259 insertions(+), 20 deletions(-) create mode 100644 report/finding-schema.json create mode 100644 report/sample-findings.json diff --git a/.gitignore b/.gitignore index bb6fef39a..cc7af077d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -7.bin/ +.bin/ .kube .vscode/ .idea/ @@ -50,3 +50,9 @@ specs/ auth/oidc/static/tailwind.min.js **/*.pem **/*.key +report/*.png +report/*.pdf +*.pdf +*.png +.playwright-mcp/ +out.* diff --git a/go.mod b/go.mod index fde809c3a..8b7d58612 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,8 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.11 github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0 github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 + github.com/charmbracelet/huh v1.0.0 + github.com/chromedp/cdproto v0.0.0-20260320225252-cf654f46fc63 github.com/chromedp/chromedp v0.15.0 github.com/emersion/go-message v0.18.2 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 @@ -165,7 +167,6 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/chromedp/cdproto v0.0.0-20260320225252-cf654f46fc63 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect diff --git a/go.sum b/go.sum index 8f5c7b0be..ad0f8c72e 100644 --- a/go.sum +++ b/go.sum @@ -287,6 +287,9 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= @@ -394,6 +397,7 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -454,7 +458,6 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01 github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -550,7 +553,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= @@ -887,6 +889,7 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= @@ -924,6 +927,7 @@ github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3P github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= @@ -1220,22 +1224,6 @@ github.com/zitadel/oidc/v3 v3.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrD github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= -gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow= -gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= -gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M= -gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw= -gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs= -go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= -go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= -go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= -go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= -go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= -go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= -go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= -go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/report/finding-schema.json b/report/finding-schema.json new file mode 100644 index 000000000..416e3fe8e --- /dev/null +++ b/report/finding-schema.json @@ -0,0 +1,438 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "audit-finding.schema.json", + "title": "Audit Log Finding", + "type": "object", + "required": ["title", "severity", "platform", "category", "outcome", "detection", "evidence", "recommendation"], + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + "maxLength": 120, + "description": "Human-readable finding title" + }, + "severity": { + "enum": ["critical", "high", "medium", "low", "info"], + "description": "Impact severity level" + }, + "platform": { + "enum": ["sql-server", "kubernetes", "aws", "azure", "mission-control"], + "description": "Source platform where the finding was detected" + }, + "outcome": { + "enum": ["safety-switch", "page-oncall", "high-ticket", "low-ticket", "informational"], + "description": "Recommended response action: safety-switch (breach confirmed, auto-contain), page-oncall (breach suspected, page team), high-ticket (open high-severity ticket), low-ticket (open low-severity ticket), informational (log for awareness)" + }, + "category": { + "enum": [ + "credential-attack", + "privilege-escalation", + "privilege-accumulation", + "data-exfiltration", + "lateral-movement", + "persistence", + "audit-tampering", + "after-hours", + "break-glass", + "shared-account", + "service-account-misuse", + "network-exposure", + "destructive-action", + "coverage-gap" + ], + "description": "Attack pattern or finding category from the kill chain" + }, + "detection": { + "type": "object", + "required": ["pattern"], + "additionalProperties": false, + "properties": { + "pattern": { + "type": "string", + "description": "Name of the detection pattern that triggered this finding, e.g. 'brute-force', 'credential-spraying'" + }, + "threshold": { + "type": "string", + "description": "Threshold that was exceeded, e.g. '>= 50 failures from single IP in < 1 hour'" + } + } + }, + "dataSource": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "Source type: s3-parquet, cloudtrail-athena, k8s-audit-log, azure-log-analytics, mission-control, file, etc." + }, + "categories": { + "type": "array", + "items": { + "enum": ["ai", "users", "groups", "roles", "access-logs", "flow-logs", "audit-logs", "configuration"] + }, + "description": "Data categories this source provides (e.g. audit-logs, access-logs, roles)" + }, + "connection": { + "type": "string", + "description": "Connection name or identifier (e.g. connection://monitoring/sql-server, AWS account ID)" + }, + "path": { + "type": "string", + "description": "Data path: S3 URI, Athena table, log file path, API endpoint" + }, + "query": { + "type": "string", + "description": "Query or filter used to extract the data" + }, + "timeRange": { + "$ref": "#/$defs/timeRange", + "description": "Time range of the data queried" + }, + "git": { + "type": "object", + "additionalProperties": false, + "properties": { + "repo": { "type": "string", "description": "Git repository URL or name" }, + "file": { "type": "string", "description": "File path within the repository" }, + "lineNo": { "type": "integer", "description": "Line number in the file" }, + "sha": { "type": "string", "description": "Git commit SHA" }, + "branch": { "type": "string", "description": "Git branch name" }, + "tag": { "type": "string", "description": "Git tag (e.g. v1.2.0)" } + } + }, + "contentSha": { + "type": "string", + "description": "SHA-256 hash of the data content for integrity verification" + }, + "app": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "type": "string", "description": "Application or scraper name (e.g. config-db, mission-control-agent)" }, + "version": { "type": "string", "description": "Application version" }, + "icon": { "type": "string", "description": "Icon name for display" } + } + }, + "file": { + "type": "object", + "additionalProperties": false, + "description": "File metadata for local/remote file data sources", + "properties": { + "name": { "type": "string", "description": "File name" }, + "size": { "type": "string", "description": "File size (e.g. 2.4MB, 156KB)" }, + "created": { "type": "string", "format": "date-time", "description": "File creation timestamp" }, + "modified": { "type": "string", "format": "date-time", "description": "File last modified timestamp" }, + "location": { "type": "string", "description": "Storage location type: local, sharepoint, google-drive, onedrive, network-share" }, + "host": { "type": "string", "description": "Host machine, SharePoint site, or drive name" } + } + } + } + }, + "evidence": { + "type": "object", + "required": ["summary"], + "additionalProperties": false, + "properties": { + "summary": { + "type": "string", + "description": "Human-readable description of what was observed" + }, + "timeRange": { + "$ref": "#/$defs/timeRange", + "description": "Time window of the observed activity" + }, + "metrics": { + "$ref": "#/$defs/metrics", + "description": "Quantitative measurements supporting the finding" + }, + "samples": { + "type": "array", + "maxItems": 10, + "items": { "$ref": "#/$defs/eventSample" }, + "description": "Representative event samples (max 10)" + } + } + }, + "recommendation": { + "type": "object", + "required": ["action"], + "additionalProperties": false, + "properties": { + "action": { + "type": "string", + "description": "Primary remediation action" + }, + "mitigations": { + "type": "array", + "items": { "type": "string" }, + "description": "Additional mitigating steps" + }, + "references": { + "type": "array", + "items": { "type": "string", "format": "uri" }, + "description": "Links to relevant documentation, CIS benchmarks, etc." + } + } + }, + "context": { + "type": "object", + "additionalProperties": false, + "properties": { + "killChainPhase": { + "enum": [ + "reconnaissance", + "initial-access", + "persistence", + "privilege-escalation", + "lateral-movement", + "collection", + "exfiltration", + "impact" + ], + "description": "MITRE ATT&CK kill chain phase" + }, + "mitreTechnique": { + "type": "string", + "pattern": "^T[0-9]{4}(\\.[0-9]{3})?$", + "description": "MITRE ATT&CK technique ID, e.g. T1078 or T1078.004" + }, + "compliance": { + "type": "array", + "items": { + "type": "string", + "description": "Compliance framework reference, e.g. 'PCI-DSS 10.2.4', 'SOX 404', 'CIS AWS 3.1'" + } + }, + "relatedFindings": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of related findings that may form a larger attack chain" + }, + "baseline": { + "type": "object", + "additionalProperties": false, + "properties": { + "normalValue": { + "type": "number", + "description": "Expected baseline value (e.g. avg events/day)" + }, + "observedValue": { + "type": "number", + "description": "Observed value during this finding" + }, + "deviationFactor": { + "type": "number", + "description": "Ratio of observed/normal (e.g. 5.2 means 5.2x the baseline)" + }, + "baselinePeriod": { + "type": "string", + "description": "Period used for baseline calculation, e.g. 'previous 4 weeks'" + } + } + } + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "generatedAt": { + "type": "string", + "format": "date-time", + "description": "When this finding was generated (ISO 8601 UTC)" + }, + "generatedBy": { + "type": "string", + "description": "Tool or agent that produced the finding (e.g. 'audit-log-analyzer', 'mission-control-scraper')" + }, + "version": { + "type": "string", + "description": "Version of the detection rules or tool" + }, + "runId": { + "type": "string", + "description": "Unique identifier for the analysis run that produced this finding" + }, + "model": { + "type": "string", + "description": "AI model used if finding was AI-generated (e.g. 'claude-opus-4-6')" + } + } + } + }, + "$defs": { + "identity": { + "type": "object", + "required": ["name", "type"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Identity name (UPN, ARN, service account name, SQL principal)" + }, + "type": { + "enum": ["human", "service-account", "break-glass", "admin", "machine", "root", "unknown"], + "description": "Identity classification" + }, + "displayName": { + "type": "string", + "description": "Resolved human-readable display name" + }, + "id": { + "type": "string", + "description": "Unique identifier (e.g. SID, ARN, UUID)" + } + } + }, + "resource": { + "type": "object", + "required": ["name", "type"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Resource identifier (database name, bucket ARN, namespace, etc.)" + }, + "type": { + "type": "string", + "description": "Resource type (database, s3-bucket, namespace, role, secret, etc.)" + }, + "id": { + "type": "string", + "description": "Unique resource ID if available (ARN, UUID, etc.)" + }, + "scope": { + "type": "string", + "description": "Scope or container (server instance, subscription, cluster, etc.), multiple resources may share the same scope" + }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "timeRange": { + "type": "object", + "required": ["start", "end"], + "additionalProperties": false, + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "Start of the observed activity (ISO 8601 UTC)" + }, + "end": { + "type": "string", + "format": "date-time", + "description": "End of the observed activity (ISO 8601 UTC)" + }, + "durationSeconds": { + "type": "integer", + "description": "Duration of the activity window in seconds" + } + } + }, + "metrics": { + "type": "object", + "additionalProperties": false, + "properties": { + "eventCount": { + "type": "integer", + "description": "Total number of events matching the detection" + }, + "failedCount": { + "type": "integer", + "description": "Number of failed attempts (logins, API calls)" + }, + "successCount": { + "type": "integer", + "description": "Number of successful events" + }, + "uniqueIdentities": { + "type": "integer", + "description": "Distinct identity count involved" + }, + "uniqueIPs": { + "type": "integer", + "description": "Distinct source IP count" + }, + "uniqueResources": { + "type": "integer", + "description": "Distinct resource count affected" + }, + "rowsAccessed": { + "type": "integer", + "description": "Total rows read or modified (SQL Server specific)" + }, + "permissionChanges": { + "type": "integer", + "description": "Number of grant/revoke/role-change operations" + } + } + }, + "actor": { + "type": "object", + "additionalProperties": false, + "properties": { + "identity": { "$ref": "#/$defs/identity" }, + "endpoint": { "$ref": "#/$defs/endpoint" }, + "app": { "$ref": "#/$defs/appRef" }, + "resource": { "$ref": "#/$defs/resource" } + } + }, + "endpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "ip": { "type": "string", "description": "IP address" }, + "hostname": { "type": "string", "description": "Hostname or FQDN" }, + "type": { "type": "string", "description": "Endpoint type (e.g. workstation, server, vpn)" }, + "network": { "type": "string", "description": "Network segment or VPC" }, + "tags": { "type": "array", "items": { "type": "string" } } + } + }, + "appRef": { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { "type": "string", "description": "Application name (e.g. SSMS, kubectl, aws-cli)" }, + "type": { "type": "string", "description": "Application type (e.g. database-client, cli, sdk)" }, + "tags": { "type": "array", "items": { "type": "string" } } + } + }, + "eventSample": { + "type": "object", + "required": ["timestamp", "action"], + "additionalProperties": false, + "properties": { + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Event timestamp (ISO 8601 UTC)" + }, + "action": { + "type": "string", + "description": "Action name or API call" + }, + "detail": { + "type": "string", + "maxLength": 500, + "description": "Statement text, request parameters, or other detail (truncated)" + }, + "succeeded": { + "type": "boolean", + "description": "Whether the action succeeded" + }, + "src": { + "$ref": "#/$defs/actor", + "description": "Source actor (who/what initiated the action)" + }, + "dst": { + "$ref": "#/$defs/actor", + "description": "Destination actor (target of the action)" + } + } + } + } +} diff --git a/report/sample-findings.json b/report/sample-findings.json new file mode 100644 index 000000000..9be675c1a --- /dev/null +++ b/report/sample-findings.json @@ -0,0 +1,806 @@ +{ + "findings": [ + { + "title": "Brute force attack against sa account from 203.0.113.42", + "severity": "critical", + "platform": "sql-server", + "category": "credential-attack", + "outcome": "page-oncall", + "detection": { + "pattern": "brute-force", + "threshold": ">= 50 failures from single IP in < 1 hour" + }, + "evidence": { + "summary": "203.0.113.42 made 847 failed login attempts against 'sa' in a 23-minute window between 02:14 and 02:37 UTC", + "timeRange": { + "start": "2026-03-15T02:14:00Z", + "end": "2026-03-15T02:37:00Z", + "durationSeconds": 1380 + }, + "metrics": { "eventCount": 847, "failedCount": 847, "successCount": 0, "uniqueIPs": 1 }, + "samples": [ + { + "timestamp": "2026-03-15T02:14:03Z", + "action": "FAILED_LOGIN_GROUP", + "detail": "Login failed for user 'sa'. Reason: Password did not match.", + "succeeded": false, + "src": { + "identity": { "name": "sa", "type": "break-glass" }, + "endpoint": { "ip": "203.0.113.42" } + }, + "dst": { + "resource": { "name": "acme_KEN_app_PROD", "type": "database", "scope": "acmer-prod-mssql" } + } + }, + { + "timestamp": "2026-03-15T02:36:58Z", + "action": "FAILED_LOGIN_GROUP", + "detail": "Login failed for user 'sa'. Reason: Password did not match.", + "succeeded": false, + "src": { + "identity": { "name": "sa", "type": "break-glass" }, + "endpoint": { "ip": "203.0.113.42" } + }, + "dst": { + "resource": { "name": "acme_KEN_app_PROD", "type": "database", "scope": "acmer-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Block 203.0.113.42 at the network level and verify sa account is disabled for remote login", + "mitigations": [ + "Ensure sa account has a strong password and is disabled for network auth", + "Implement account lockout policy after 5 consecutive failures", + "Add IP-based rate limiting at the network/WAF layer" + ] + }, + "context": { + "killChainPhase": "initial-access", + "mitreTechnique": "T1110", + "compliance": ["PCI-DSS 10.2.4", "SOX 404"], + "baseline": { "normalValue": 2, "observedValue": 847, "deviationFactor": 423.5, "baselinePeriod": "previous 4 weeks" } + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["access-logs"], + "path": "s3://acme-app-audit/sql/parquet/AuditLogins/2026/03/*/*.parquet", + "query": "SELECT server_principal_name, client_ip, count(*) AS failed_count FROM AuditLogins WHERE succeeded = false GROUP BY server_principal_name, client_ip HAVING failed_count >= 50", + "connection": "connection://monitoring/sql-server", + "app": { "name": "config-db", "version": "1.4.2", "icon": "config-db" }, + "contentSha": "a3f8e2d1b4c6a9e0f7d5b3c1a8e6f4d2b0c7a5e3f1d9b7c5a3e1f0d8b6c4a2" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "Credential spraying from 198.51.100.17 targeting 12 accounts", + "severity": "high", + "platform": "sql-server", + "category": "credential-attack", + "outcome": "high-ticket", + "detection": { + "pattern": "credential-spraying", + "threshold": ">= 10 distinct accounts from 1 IP" + }, + "evidence": { + "summary": "198.51.100.17 attempted login to 12 distinct accounts over 45 minutes with 1-2 attempts each, consistent with password spray pattern", + "timeRange": { + "start": "2026-03-18T14:22:00Z", + "end": "2026-03-18T15:07:00Z", + "durationSeconds": 2700 + }, + "metrics": { "eventCount": 18, "failedCount": 18, "successCount": 0, "uniqueIdentities": 12, "uniqueIPs": 1 }, + "samples": [ + { + "timestamp": "2026-03-18T14:22:00Z", + "action": "credential-spraying", + "succeeded": false, + "src": { + "identity": { "name": "jsmith", "type": "human" }, + "endpoint": { "ip": "198.51.100.17" } + } + }, + { + "timestamp": "2026-03-18T14:22:00Z", + "action": "credential-spraying", + "succeeded": false, + "src": { + "identity": { "name": "admin_dba", "type": "admin" }, + "endpoint": { "ip": "198.51.100.17" } + } + }, + { + "timestamp": "2026-03-18T14:22:00Z", + "action": "credential-spraying", + "succeeded": false, + "src": { + "identity": { "name": "svc_app_app", "type": "service-account" }, + "endpoint": { "ip": "198.51.100.17" } + } + } + ] + }, + "recommendation": { + "action": "Investigate source IP 198.51.100.17 and force password reset for all targeted accounts", + "mitigations": [ + "Enable multi-factor authentication for all database accounts", + "Review VPN/network access logs for 198.51.100.17" + ] + }, + "context": { + "killChainPhase": "initial-access", + "mitreTechnique": "T1110.003", + "compliance": ["PCI-DSS 10.2.4"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["access-logs"], + "path": "s3://acmer-app-audit/sql/parquet/AuditLogins/2026/03/*/*.parquet", + "query": "SELECT client_ip, count(DISTINCT server_principal_name) AS targeted FROM AuditLogins WHERE succeeded = false GROUP BY client_ip HAVING targeted >= 3", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "sysadmin role granted to jdoe outside change window", + "severity": "critical", + "platform": "sql-server", + "category": "privilege-escalation", + "outcome": "safety-switch", + "detection": { + "pattern": "role-membership-change", + "threshold": "Any sysadmin role grant" + }, + "evidence": { + "summary": "User admin_dba granted sysadmin role to jdoe at 22:41 UTC on a Tuesday, outside the approved change window (Wed 06:00-10:00 UTC)", + "timeRange": { + "start": "2026-03-11T22:41:00Z", + "end": "2026-03-11T22:41:00Z", + "durationSeconds": 0 + }, + "metrics": { "eventCount": 1, "permissionChanges": 1 }, + "samples": [ + { + "timestamp": "2026-03-11T22:41:12Z", + "action": "SERVER_ROLE_MEMBER_CHANGE_GROUP", + "detail": "ALTER SERVER ROLE [sysadmin] ADD MEMBER [jdoe]", + "succeeded": true, + "src": { + "identity": { "name": "admin_dba", "type": "admin", "displayName": "Database Administrator" }, + "endpoint": { "ip": "10.0.5.23" } + }, + "dst": { + "identity": { "name": "jdoe", "type": "human", "displayName": "John Doe" }, + "resource": { "name": "sysadmin", "type": "role", "scope": "acme-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Immediately revoke jdoe's sysadmin membership and investigate why it was granted outside change window", + "mitigations": [ + "Require dual-approval for sysadmin role grants", + "Implement change window enforcement via audit alerts" + ] + }, + "context": { + "killChainPhase": "privilege-escalation", + "mitreTechnique": "T1078.004", + "compliance": ["SOX 404", "PCI-DSS 10.2.5", "POPI Act"], + "relatedFindings": ["HOURS-2026-001"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["audit-logs", "roles"], + "path": "s3://acmer-app-audit/sql/parquet/AuditServer/2026/03/*/*.parquet", + "query": "SELECT * FROM AuditServer WHERE containing_group_name = 'SERVER_ROLE_MEMBER_CHANGE_GROUP' AND statement LIKE '%sysadmin%'", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "Bulk data extraction by svc_reporting \u2014 2.4M rows in single query", + "severity": "high", + "platform": "sql-server", + "category": "data-exfiltration", + "outcome": "page-oncall", + "detection": { + "pattern": "bulk-data-read", + "threshold": "response_rows > 10x user baseline" + }, + "evidence": { + "summary": "Service account svc_reporting executed SELECT * FROM PolicyHolder returning 2.4M rows at 03:12 UTC. Normal daily volume is ~15K rows.", + "timeRange": { + "start": "2026-03-20T03:12:00Z", + "end": "2026-03-20T03:14:32Z", + "durationSeconds": 152 + }, + "metrics": { "eventCount": 1, "rowsAccessed": 2400000 }, + "samples": [ + { + "timestamp": "2026-03-20T03:12:05Z", + "action": "SELECT", + "detail": "SELECT [PolicyHolderID], [FirstName], [LastName], [IDNumber], [DateOfBirth], [Email], [Phone], [BankAccountNo], [BankBranchCode], [PolicyNumber], [PremiumAmount], [BeneficiaryName], [BeneficiaryID], [MedicalHistory], [ClaimStatus] FROM [dbo].[PolicyHolder] WITH (NOLOCK) WHERE [Status] = 'Active' ORDER BY [PolicyHolderID]", + "succeeded": true, + "src": { + "identity": { "name": "svc_reporting", "type": "service-account" }, + "endpoint": { "ip": "10.0.12.88" }, + "app": { "name": "ReportingETL.exe", "type": "etl-tool" } + }, + "dst": { + "resource": { "name": "PolicyHolder", "type": "database", "scope": "OMA_ZIM_app_PROD" } + } + } + ] + }, + "recommendation": { + "action": "Investigate svc_reporting activity \u2014 verify this query was from an authorized report job, not compromised credentials", + "mitigations": [ + "Restrict svc_reporting to specific columns via views instead of SELECT *", + "Implement row-level security on PolicyHolder table", + "Add query result size limits to the application connection" + ] + }, + "context": { + "killChainPhase": "collection", + "mitreTechnique": "T1530", + "compliance": ["POPI Act", "PCI-DSS 10.2.1"], + "baseline": { "normalValue": 15000, "observedValue": 2400000, "deviationFactor": 160.0, "baselinePeriod": "previous 4 weeks" } + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["audit-logs"], + "path": "s3://acme-app-audit/sql/parquet/AuditUserActivity/2026/03/*/*.parquet", + "query": "SELECT server_principal_name, response_rows, statement FROM AuditUserActivity WHERE response_rows > 10000 ORDER BY response_rows DESC", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "Break-glass account acmedmin used without incident ticket", + "severity": "critical", + "platform": "sql-server", + "category": "break-glass", + "outcome": "page-oncall", + "detection": { + "pattern": "break-glass-usage", + "threshold": "Any usage of break-glass account" + }, + "evidence": { + "summary": "Break-glass account 'acmedmin' logged in 3 times from 10.0.5.23 on March 22. No corresponding incident ticket found in ServiceNow.", + "timeRange": { + "start": "2026-03-22T09:15:00Z", + "end": "2026-03-22T11:42:00Z", + "durationSeconds": 8820 + }, + "metrics": { "eventCount": 3, "successCount": 3 }, + "samples": [ + { + "timestamp": "2026-03-22T09:15:00Z", + "action": "break-glass-usage", + "succeeded": true, + "src": { + "identity": { "name": "acmedmin", "type": "break-glass" }, + "endpoint": { "ip": "10.0.5.23" } + }, + "dst": { + "resource": { "name": "OMA_KEN_app_PROD", "type": "database", "scope": "acme-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Investigate who used the acmedmin account and for what purpose. Create retroactive incident ticket.", + "mitigations": [ + "Rotate acmedmin password immediately", + "Require ServiceNow ticket ID before break-glass credentials are issued", + "Implement just-in-time access provisioning" + ] + }, + "context": { + "killChainPhase": "initial-access", + "mitreTechnique": "T1078", + "compliance": ["SOX 404", "King IV"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["access-logs", "audit-logs"], + "path": "s3://acme-app-audit/sql/parquet/AuditLogins/2026/03/*/*.parquet", + "query": "SELECT * FROM AuditLogins WHERE server_principal_name IN ('sa', 'acmedmin')", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "SQL Server audit configuration modified by admin_dba", + "severity": "critical", + "platform": "sql-server", + "category": "audit-tampering", + "outcome": "safety-switch", + "detection": { + "pattern": "audit-config-change", + "threshold": "Any audit configuration change" + }, + "evidence": { + "summary": "admin_dba modified the AuditUserActivity server audit at 01:33 UTC, changing the WHERE clause to exclude client_ip LIKE '192.168.%'", + "timeRange": { + "start": "2026-03-25T01:33:00Z", + "end": "2026-03-25T01:33:00Z", + "durationSeconds": 0 + }, + "metrics": { "eventCount": 1 }, + "samples": [ + { + "timestamp": "2026-03-25T01:33:45Z", + "action": "AUDIT_CHANGE_GROUP", + "detail": "ALTER SERVER AUDIT [AuditUserActivity] WITH (STATE = ON, QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE) WHERE ([client_ip] NOT LIKE '192.168.%' AND [server_principal_name] NOT IN ('svc_monitoring', 'svc_backup', 'svc_replication'))", + "succeeded": true, + "src": { + "identity": { "name": "admin_dba", "type": "admin" }, + "endpoint": { "ip": "10.0.5.23" }, + "app": { "name": "sqlcmd", "type": "database-client" } + }, + "dst": { + "resource": { "name": "AuditUserActivity", "type": "audit-type", "scope": "acme-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Revert the audit configuration change immediately and investigate admin_dba's intent", + "mitigations": [ + "Restrict ALTER SERVER AUDIT permissions to break-glass accounts only", + "Alert on any AUDIT_CHANGE_GROUP event in real-time" + ] + }, + "context": { + "killChainPhase": "persistence", + "mitreTechnique": "T1562.002", + "compliance": ["SOX 404", "PCI-DSS 10.5"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["audit-logs", "configuration"], + "path": "s3://acme-app-audit/sql/parquet/AuditServer/2026/03/*/*.parquet", + "query": "SELECT * FROM AuditServer WHERE containing_group_name IN ('AUDIT_CHANGE_GROUP', 'TRACE_CHANGE_GROUP')", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "After-hours DDL activity by admin_dba \u2014 14 schema changes at 22:30 UTC", + "severity": "high", + "platform": "sql-server", + "category": "after-hours", + "outcome": "high-ticket", + "detection": { + "pattern": "after-hours-ddl", + "threshold": "DDL outside business hours (06:00-16:00 UTC)" + }, + "evidence": { + "summary": "admin_dba executed 14 DDL statements between 22:30 and 23:15 UTC on March 11, including ALTER TABLE and CREATE INDEX operations on production tables", + "timeRange": { + "start": "2026-03-11T22:30:00Z", + "end": "2026-03-11T23:15:00Z", + "durationSeconds": 2700 + }, + "metrics": { "eventCount": 14 }, + "samples": [ + { + "timestamp": "2026-03-11T22:30:00Z", + "action": "after-hours-ddl", + "succeeded": true, + "src": { + "identity": { "name": "admin_dba", "type": "admin", "displayName": "Database Administrator" }, + "endpoint": { "ip": "10.0.5.23" } + }, + "dst": { + "resource": { "name": "acme_KEN_app_PROD", "type": "database", "scope": "acme-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Verify DDL changes were authorized through change management process", + "mitigations": ["Enforce change windows via database triggers or alerts"] + }, + "context": { + "killChainPhase": "impact", + "compliance": ["SOX 404"], + "relatedFindings": ["PRIV-2026-001"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["audit-logs"], + "path": "s3://acme-app-audit/sql/parquet/AuditServer/2026/03/*/*.parquet", + "query": "SELECT * FROM AuditServer WHERE toHour(event_time) NOT BETWEEN 6 AND 16", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "ClusterRoleBinding granting cluster-admin created in production", + "severity": "critical", + "platform": "kubernetes", + "category": "privilege-escalation", + "outcome": "safety-switch", + "detection": { + "pattern": "cluster-admin-binding", + "threshold": "Any cluster-admin ClusterRoleBinding creation" + }, + "evidence": { + "summary": "User devops@acme.com created ClusterRoleBinding 'emergency-access' granting cluster-admin to service account 'debug-sa' in namespace 'default'", + "timeRange": { + "start": "2026-03-19T16:22:00Z", + "end": "2026-03-19T16:22:00Z", + "durationSeconds": 0 + }, + "metrics": { "eventCount": 1, "permissionChanges": 1 }, + "samples": [ + { + "timestamp": "2026-03-19T16:22:00Z", + "action": "create", + "detail": "kubectl create clusterrolebinding emergency-access --clusterrole=cluster-admin --serviceaccount=default:debug-sa", + "succeeded": true, + "src": { + "identity": { "name": "devops@acme.com", "type": "human" }, + "endpoint": { "ip": "10.0.3.45" }, + "app": { "name": "kubectl", "type": "cli" } + }, + "dst": { + "identity": { "name": "system:serviceaccount:default:debug-sa", "type": "service-account" }, + "resource": { "name": "emergency-access", "type": "clusterrolebinding", "scope": "acme-prod" } + } + } + ] + }, + "recommendation": { + "action": "Delete the emergency-access ClusterRoleBinding and investigate why cluster-admin access was needed", + "mitigations": [ + "Use namespace-scoped RoleBindings instead of ClusterRoleBindings", + "Implement OPA/Gatekeeper policy to block cluster-admin bindings" + ] + }, + "context": { + "killChainPhase": "privilege-escalation", + "mitreTechnique": "T1078.004", + "compliance": ["CIS Kubernetes 5.1.1"] + }, + "dataSource": { + "type": "k8s-audit-log", + "categories": ["audit-logs", "roles", "configuration"], + "path": "kubernetes audit log (acme-prod cluster)", + "query": "cat audit.log | jq 'select(.verb==\"create\" and .objectRef.resource==\"clusterrolebindings\")'", + "connection": "omar-prod", + "git": { + "repo": "github.com/acme/infra-k8s", + "file": "clusters/prod/audit-policy.yaml", + "lineNo": 42, + "sha": "e7f3a2b1c9d8e6f4a0b5c3d1", + "branch": "main", + "tag": "v2.1.0" + }, + "app": { "name": "mission-control", "version": "2.3.1", "icon": "mission-control" } + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "Security group opened to 0.0.0.0/0 on port 1433", + "severity": "critical", + "platform": "aws", + "category": "network-exposure", + "outcome": "safety-switch", + "detection": { + "pattern": "security-group-exposure", + "threshold": "Inbound 0.0.0.0/0 on ports 22, 1433, 3306, 3389" + }, + "evidence": { + "summary": "IAM user infra-deploy added inbound rule to sg-0a1b2c3d allowing 0.0.0.0/0 on TCP/1433 (SQL Server), exposing the RDS instance to the internet", + "timeRange": { + "start": "2026-03-28T08:45:00Z", + "end": "2026-03-28T08:45:00Z", + "durationSeconds": 0 + }, + "metrics": { "eventCount": 1 }, + "samples": [ + { + "timestamp": "2026-03-28T08:45:00Z", + "action": "AuthorizeSecurityGroupIngress", + "detail": "IpPermissions: [{IpProtocol: tcp, FromPort: 1433, ToPort: 1433, IpRanges: [{CidrIp: 0.0.0.0/0}]}]", + "succeeded": true, + "src": { + "identity": { "name": "arn:aws:iam::123456789012:user/infra-deploy", "type": "service-account" }, + "endpoint": { "ip": "10.0.1.100" }, + "app": { "name": "terraform", "type": "iac-tool" } + }, + "dst": { + "resource": { "name": "sg-0a1b2c3d", "type": "security-group", "scope": "vpc-abc123 (eu-west-1)" } + } + } + ] + }, + "recommendation": { + "action": "Immediately remove the 0.0.0.0/0 ingress rule on port 1433 and restrict to VPC CIDR only", + "mitigations": [ + "Enable AWS Config rule to detect public security group rules", + "Use VPC endpoints or PrivateLink for database access" + ] + }, + "context": { + "killChainPhase": "initial-access", + "mitreTechnique": "T1190", + "compliance": ["CIS AWS 5.2", "PCI-DSS 1.3.1"] + }, + "dataSource": { + "type": "cloudtrail-athena", + "categories": ["audit-logs", "configuration"], + "path": "CloudTrail (eu-west-1)", + "query": "SELECT * FROM cloudtrail_logs WHERE eventName = 'AuthorizeSecurityGroupIngress'", + "connection": "123456789012", + "app": { "name": "aws-scraper", "version": "3.0.1", "icon": "aws" }, + "contentSha": "b7c4d2e0f8a6b3c1d9e5f2a0c8d4e1b6a3f7c0d5e9b2a4f6c8d1e3b5a7f0c2" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "Privilege accumulation \u2014 23 grants with 0 revokes over 4 weeks", + "severity": "medium", + "platform": "sql-server", + "category": "privilege-accumulation", + "outcome": "low-ticket", + "detection": { + "pattern": "grant-revoke-imbalance", + "threshold": "Net positive accumulation > 4 consecutive weeks" + }, + "evidence": { + "summary": "Over the past 4 weeks, 23 GRANT operations and 0 REVOKE operations were recorded across 3 databases, indicating unchecked privilege creep", + "timeRange": { + "start": "2026-03-01T00:00:00Z", + "end": "2026-03-31T23:59:59Z", + "durationSeconds": 2678399 + }, + "metrics": { "eventCount": 23, "permissionChanges": 23, "uniqueResources": 3 }, + "samples": [ + { + "timestamp": "2026-03-01T00:00:00Z", + "action": "grant-revoke-imbalance", + "src": { + "identity": { "name": "admin_dba", "type": "admin" } + }, + "dst": { + "resource": { "name": "acme_KEN_app_PROD", "type": "database", "scope": "acme-prod-mssql" } + } + }, + { + "timestamp": "2026-03-01T00:00:00Z", + "action": "grant-revoke-imbalance", + "src": { + "identity": { "name": "lead_dev", "type": "human" } + }, + "dst": { + "resource": { "name": "acme_ZIM_app_PROD", "type": "database", "scope": "acme-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Conduct a privilege review across all three databases \u2014 identify and revoke unnecessary grants", + "mitigations": [ + "Schedule quarterly access reviews", + "Implement role-based access with defined permission sets" + ] + }, + "context": { + "killChainPhase": "privilege-escalation", + "compliance": ["SOX 404", "King IV"] + }, + "dataSource": { + "type": "file", + "categories": ["roles", "users"], + "path": "Audit Reports/Q1-2026/Q1-2026-Access-Review.xlsx", + "app": { "name": "access-reviewer", "version": "1.0.0", "icon": "file" }, + "contentSha": "d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4", + "file": { + "name": "Q1-2026-Access-Review.xlsx", + "size": "2.4 MB", + "created": "2026-03-01T08:00:00Z", + "modified": "2026-03-31T16:45:00Z", + "location": "sharepoint", + "host": "acme.sharepoint.com/sites/SecurityTeam" + } + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "Service account svc_app_app connected via SSMS from developer workstation", + "severity": "high", + "platform": "sql-server", + "category": "service-account-misuse", + "outcome": "high-ticket", + "detection": { + "pattern": "interactive-service-account", + "threshold": "Service account used with interactive tool" + }, + "evidence": { + "summary": "svc_app_app connected using SQL Server Management Studio from IP 10.0.8.77 (developer subnet) 7 times over 2 hours", + "timeRange": { + "start": "2026-03-26T13:10:00Z", + "end": "2026-03-26T15:22:00Z", + "durationSeconds": 7920 + }, + "metrics": { "eventCount": 7, "successCount": 7, "uniqueIPs": 1 }, + "samples": [ + { + "timestamp": "2026-03-26T13:10:00Z", + "action": "LOGIN", + "succeeded": true, + "src": { + "identity": { "name": "svc_app_app", "type": "service-account" }, + "endpoint": { "ip": "10.0.8.77", "type": "workstation", "network": "developer-subnet" }, + "app": { "name": "SQL Server Management Studio", "type": "database-client" } + }, + "dst": { + "endpoint": { "ip": "10.0.2.15", "type": "server", "hostname": "acme-prod-mssql" }, + "resource": { "name": "acme_KEN_app_PROD", "type": "database", "scope": "acme-prod-mssql" } + } + }, + { + "timestamp": "2026-03-26T14:05:00Z", + "action": "SELECT", + "detail": "SELECT TOP 100 * FROM [dbo].[Claims] ORDER BY ClaimDate DESC", + "succeeded": true, + "src": { + "identity": { "name": "svc_app_app", "type": "service-account" }, + "endpoint": { "ip": "10.0.8.77", "type": "workstation" }, + "app": { "name": "SQL Server Management Studio", "type": "database-client" } + }, + "dst": { + "resource": { "name": "Claims", "type": "database", "scope": "acme_KEN_app_PROD" } + } + } + ] + }, + "recommendation": { + "action": "Rotate svc_app_app credentials and investigate who on the developer subnet has access to this service account password", + "mitigations": [ + "Store service account credentials in a vault with audit trail", + "Restrict service accounts to connect only from application server IPs" + ] + }, + "context": { + "killChainPhase": "lateral-movement", + "mitreTechnique": "T1078.001", + "compliance": ["PCI-DSS 10.2.5"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["access-logs"], + "path": "s3://acme-app-audit/sql/parquet/AuditLogins/2026/03/*/*.parquet", + "query": "SELECT * FROM AuditLogins WHERE server_principal_name LIKE '%svc%' AND application_name LIKE '%Management Studio%'", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + }, + { + "title": "3-day gap in AuditUserActivity coverage \u2014 March 8-10", + "severity": "high", + "platform": "sql-server", + "category": "coverage-gap", + "outcome": "high-ticket", + "detection": { + "pattern": "audit-coverage-gap", + "threshold": "Any day with zero events" + }, + "evidence": { + "summary": "No AuditUserActivity parquet files exist for March 8, 9, and 10. AuditLogins and AuditServer have normal coverage for these dates, suggesting a selective audit failure.", + "timeRange": { + "start": "2026-03-08T00:00:00Z", + "end": "2026-03-10T23:59:59Z", + "durationSeconds": 259199 + }, + "metrics": { "eventCount": 0 }, + "samples": [ + { + "timestamp": "2026-03-08T00:00:00Z", + "action": "audit-coverage-gap", + "dst": { + "resource": { "name": "AuditUserActivity", "type": "audit-type", "scope": "acme-prod-mssql" } + } + } + ] + }, + "recommendation": { + "action": "Investigate why AuditUserActivity stopped collecting for 3 days \u2014 check SQL Server error logs and audit status", + "mitigations": [ + "Add monitoring canary for audit file freshness (existing s3-audit canary covers this)", + "Set up alerting when daily audit file count drops below threshold" + ] + }, + "context": { + "killChainPhase": "persistence", + "mitreTechnique": "T1562.002", + "compliance": ["SOX 404", "PCI-DSS 10.5"] + }, + "dataSource": { + "type": "s3-parquet", + "categories": ["audit-logs"], + "path": "s3://acme-app-audit/sql/parquet/AuditUserActivity/2026/03/*/*.parquet", + "query": "SELECT toDate(event_time) AS day, count(*) AS events FROM AuditUserActivity GROUP BY day ORDER BY day", + "connection": "connection://monitoring/sql-server" + }, + "provenance": { + "generatedAt": "2026-04-01T10:00:00Z", + "generatedBy": "audit-log-analyzer", + "version": "1.0.0", + "runId": "run-2026-04-01-001", + "model": "claude-opus-4-6" + } + } + ] +} From 77ff87469234b9726762eb5fb271d14115bd1a2a Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Wed, 15 Apr 2026 11:57:50 +0300 Subject: [PATCH 40/48] feat(api): Add MCP metadata field to ViewSpec for LLM tool registration Add MCP field to ViewSpec to enable tool registration with LLM clients (Claude, Gemini, Codex). This allows views to be properly exposed and configured when used as tools in language model integrations. Updates include: - New MCP field in ViewSpec with kubebuilder validation marker - Generated deepcopy implementation for MCP field - Updated CRD schema with MCP metadata description - Updated JSON schema with MCPMetadata reference --- api/v1/view_types.go | 4 ++++ api/v1/zz_generated.deepcopy.go | 1 + config/crds/mission-control.flanksource.com_views.yaml | 4 +--- config/schemas/view.schema.json | 4 ++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/api/v1/view_types.go b/api/v1/view_types.go index e382c2c6c..19bd500bf 100644 --- a/api/v1/view_types.go +++ b/api/v1/view_types.go @@ -112,6 +112,10 @@ type ViewSpec struct { Sections []api.ViewSection `json:"sections,omitempty" yaml:"sections,omitempty"` PDF *FacetOptions `json:"pdf,omitempty" yaml:"pdf,omitempty"` + + // MCP metadata for tool registration with LLM clients. + //+kubebuilder:validation:Optional + MCP MCPMetadata `json:"mcp,omitempty" yaml:"mcp,omitempty"` } type ViewQueryWithColumnDefs struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 6aedc972d..94f2b0253 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -3947,6 +3947,7 @@ func (in *ViewSpec) DeepCopyInto(out *ViewSpec) { *out = new(FacetOptions) (*in).DeepCopyInto(*out) } + in.MCP.DeepCopyInto(&out.MCP) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ViewSpec. diff --git a/config/crds/mission-control.flanksource.com_views.yaml b/config/crds/mission-control.flanksource.com_views.yaml index c71a8151c..3c3b2b5bc 100644 --- a/config/crds/mission-control.flanksource.com_views.yaml +++ b/config/crds/mission-control.flanksource.com_views.yaml @@ -386,9 +386,7 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true mcp: - description: |- - MCP defines metadata for MCP tool registration, controlling how - this view appears to LLM clients (Claude, Gemini, Codex). + description: MCP metadata for tool registration with LLM clients. properties: description: description: Description provides additional context for LLMs diff --git a/config/schemas/view.schema.json b/config/schemas/view.schema.json index d6ed86a7a..ce95859a8 100644 --- a/config/schemas/view.schema.json +++ b/config/schemas/view.schema.json @@ -1364,6 +1364,10 @@ }, "pdf": { "$ref": "#/$defs/FacetOptions" + }, + "mcp": { + "$ref": "#/$defs/MCPMetadata", + "description": "MCP metadata for tool registration with LLM clients." } }, "additionalProperties": false, From afdf2b849b905fddd904aaa5f50da6761fa6cf0e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Wed, 15 Apr 2026 13:54:31 +0300 Subject: [PATCH 41/48] ci: add gavel configuration for pre-build dependencies Add .gavel.yaml configuration file to define pre-build steps that run `make build` to ensure dependencies are installed before test execution. --- .gavel.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gavel.yaml diff --git a/.gavel.yaml b/.gavel.yaml new file mode 100644 index 000000000..78e419d45 --- /dev/null +++ b/.gavel.yaml @@ -0,0 +1,2 @@ +pre: + - run: make build From d308c87bc238eb788a1bda3afcf96e96266be424 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Wed, 15 Apr 2026 16:41:25 +0300 Subject: [PATCH 42/48] fix(report): Add nil check for CEL compilation issues and remove unused functions Add defensive nil check before calling issues.Err() in CEL program compilation to prevent potential nil pointer dereference. Remove unused buildEntry wrapper function and buildConfigGroups/sortedConfigIDs helper functions that are no longer needed. Update @flanksource/facet dependency from local file path to published version ^0.1.32 and add iconify icon set dependencies for enhanced icon support. --- report/catalog/change_mappings.go | 2 +- report/catalog/report.go | 50 -- report/package-lock.json | 964 +++++++++++++++++------------- report/package.json | 2 +- 4 files changed, 564 insertions(+), 454 deletions(-) diff --git a/report/catalog/change_mappings.go b/report/catalog/change_mappings.go index 94962f31f..3c46ddf89 100644 --- a/report/catalog/change_mappings.go +++ b/report/catalog/change_mappings.go @@ -89,7 +89,7 @@ func newChangeMapper(ctx dutyContext.Context, mappings []reportAPI.CatalogReport func compileChangeMappingProgram(env *cel.Env, expression string) (cel.Program, error) { ast, issues := env.Compile(expression) - if issues.Err() != nil { + if issues != nil && issues.Err() != nil { return nil, issues.Err() } diff --git a/report/catalog/report.go b/report/catalog/report.go index f1eb2eeaa..e62425302 100644 --- a/report/catalog/report.go +++ b/report/catalog/report.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "math" - "slices" "strings" "time" @@ -180,10 +179,6 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) return report, scraperIDs, nil } -func buildEntry(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time) (*api.CatalogReportEntry, []string, error) { - return buildEntryWithMapper(ctx, config, opts, sinceTime, nil) -} - func buildEntryWithMapper(ctx context.Context, config *models.ConfigItem, opts Options, sinceTime time.Time, mapper *changeMapper) (*api.CatalogReportEntry, []string, error) { entry := &api.CatalogReportEntry{ ConfigItem: api.NewCatalogReportConfigItem(*config), @@ -378,51 +373,6 @@ func decodeJSONMap(raw dutyTypes.JSON) map[string]any { return decoded } -func buildConfigGroups(report *api.CatalogReport, configMap map[uuid.UUID]models.ConfigItem) []api.CatalogReportConfigGroup { - changesByConfig := lo.GroupBy(report.Changes, func(c api.CatalogReportChange) string { return c.ConfigID }) - analysesByConfig := lo.GroupBy(report.Analyses, func(a api.CatalogReportAnalysis) string { return a.ConfigID }) - accessByConfig := lo.GroupBy(report.Access, func(a api.CatalogReportAccess) string { return a.ConfigID }) - logsByConfig := lo.GroupBy(report.AccessLogs, func(l api.CatalogReportAccessLog) string { return l.ConfigID }) - - seen := make(map[string]bool) - var groups []api.CatalogReportConfigGroup - - for _, id := range sortedConfigIDs(configMap) { - idStr := id.String() - if seen[idStr] { - continue - } - seen[idStr] = true - - changes := changesByConfig[idStr] - analyses := analysesByConfig[idStr] - access := accessByConfig[idStr] - logs := logsByConfig[idStr] - - if len(changes) == 0 && len(analyses) == 0 && len(access) == 0 && len(logs) == 0 { - continue - } - - ci := configMap[id] - groups = append(groups, api.CatalogReportConfigGroup{ - ConfigItem: api.NewCatalogReportConfigItem(ci), - Changes: changes, - Analyses: analyses, - Access: access, - AccessLogs: logs, - }) - } - return groups -} - -func sortedConfigIDs(m map[uuid.UUID]models.ConfigItem) []uuid.UUID { - ids := lo.Keys(m) - slices.SortFunc(ids, func(a, b uuid.UUID) int { - return strings.Compare(m[a].GetName(), m[b].GetName()) - }) - return ids -} - // resolveParents derives report ancestry from config.Path to avoid recursive // ParentID walks that can loop forever on cyclic catalog data. func resolveParents(ctx context.Context, config *models.ConfigItem) []models.ConfigItem { diff --git a/report/package-lock.json b/report/package-lock.json index c98598b95..610a1cf55 100644 --- a/report/package-lock.json +++ b/report/package-lock.json @@ -8,8 +8,18 @@ "name": "application-report", "version": "1.0.0", "dependencies": { - "@flanksource/facet": "file:/Users/moshe/go/src/github.com/flanksource/facet", + "@flanksource/facet": "^0.1.32", "@flanksource/icons": "^1.0.53", + "@iconify-json/carbon": "^1.2.0", + "@iconify-json/fluent": "^1.2.0", + "@iconify-json/iconoir": "^1.2.0", + "@iconify-json/lucide": "^1.2.0", + "@iconify-json/mdi": "^1.2.0", + "@iconify-json/ph": "^1.2.0", + "@iconify-json/ri": "^1.2.0", + "@iconify-json/tabler": "^1.2.0", + "@iconify-json/vscode-icons": "^1.2.45", + "@iconify/react": "^5.1.0", "js-yaml": "^4.1.0" }, "devDependencies": { @@ -25,7 +35,7 @@ }, "../../facet": { "name": "@flanksource/facet", - "version": "0.1.27", + "version": "0.1.32", "dependencies": { "@flanksource/icons": "^1.0.53", "@iconify/react": "^5.1.0", @@ -538,6 +548,465 @@ "react": "*" } }, + "node_modules/@iconify-json/carbon": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.20.tgz", + "integrity": "sha512-wqyxKEbIRdzGdfCAwQqn8iSfO6jx0m1toZAAQdx1NFjxd6iFl1YY4eKI1woWt7XOxs7s7phMW530kDD867JZGw==", + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/fluent": { + "version": "1.2.45", + "resolved": "https://registry.npmjs.org/@iconify-json/fluent/-/fluent-1.2.45.tgz", + "integrity": "sha512-gdlHc/HpvogYxocfCCg46V3A0wtAsGVBRr3FtTTmUnUyF4kaTnYe2cxdPOUj+gUBK8PZlvXqTmrQ5GTt20f4Jw==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/iconoir": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@iconify-json/iconoir/-/iconoir-1.2.10.tgz", + "integrity": "sha512-NnbdB9S5G++6wE5aEZhzpFR0HRcaZFSbJJIHOGF2axaNVKnSUs4NBW2z0uhZnM00iUkiK848Sp81EZPg52DL+w==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/lucide": { + "version": "1.2.102", + "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.102.tgz", + "integrity": "sha512-Dm3EEqu5NrmzyDMB2U1+8yroEj2/dB9V4KlH0m/szwwF/ofSf0cPaGTZqkd1aExXjCor+vU53ttRMCGuXf+/cg==", + "license": "ISC", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/mdi": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@iconify-json/mdi/-/mdi-1.2.3.tgz", + "integrity": "sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==", + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/ph": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/ph/-/ph-1.2.2.tgz", + "integrity": "sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/ri": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.2.10.tgz", + "integrity": "sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==", + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/tabler": { + "version": "1.2.33", + "resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.33.tgz", + "integrity": "sha512-q9nUQfE/cjIrGh5bAKHTphitAZpT0kX9SxDgZo3Sx8ofeDTsaHVdRwrn+CfKiJ5vQ1b1btqVwizXzIgz9KEPjA==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/vscode-icons": { + "version": "1.2.45", + "resolved": "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.2.45.tgz", + "integrity": "sha512-ow+ueibMIq79ueM1kv6cOWgHx8jfh1XJQi2RrqMHb4HLbvIBlxpy5PCMvOJXlA68R6fBAHpWQeh6uWx7VKEVsA==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/react": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@iconify/react/-/react-5.2.1.tgz", + "integrity": "sha512-37GDR3fYDZmnmUn9RagyaX+zca24jfVOMY8E1IXTqJuE8pxNtN51KWPQe3VODOWvuUurq7q9uUu3CFrpqj5Iqg==", + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -551,10 +1020,34 @@ "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -603,6 +1096,24 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -661,164 +1172,11 @@ "loose-envify": "cli.js" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", - "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", - "license": "MIT", - "dependencies": { - "emoji-regex-xs": "^1.0.0", - "regex": "^5.1.1", - "regex-recursion": "^5.1.1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/pdf-lib": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", - "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", - "license": "MIT", - "dependencies": { - "@pdf-lib/standard-fonts": "^1.0.0", - "@pdf-lib/upng": "^1.0.1", - "pako": "^1.0.11", - "tslib": "^1.11.1" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -832,6 +1190,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -856,16 +1215,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -879,6 +1228,13 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -889,95 +1245,57 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/shiki": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", - "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "1.29.2", - "@shikijs/engine-javascript": "1.29.2", - "@shikijs/engine-oniguruma": "1.29.2", - "@shikijs/langs": "1.29.2", - "@shikijs/themes": "1.29.2", - "@shikijs/types": "1.29.2", - "@shikijs/vscode-textmate": "^10.0.1", - "@types/hast": "^3.0.4" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "@types/estree": "1.0.8" }, - "engines": { - "node": ">=12" + "bin": { + "rollup": "dist/bin/rollup" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" } }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fdir": "^6.5.0", "picomatch": ">=4.0.4" @@ -989,28 +1307,13 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -1046,117 +1349,12 @@ "dev": true, "license": "MIT" }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -1225,44 +1423,6 @@ "optional": true } } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } } } } diff --git a/report/package.json b/report/package.json index 8122da3af..92f1fe2e4 100644 --- a/report/package.json +++ b/report/package.json @@ -9,7 +9,7 @@ "mission-control": "npm run pdf" }, "dependencies": { - "@flanksource/facet": "file:/Users/moshe/go/src/github.com/flanksource/facet", + "@flanksource/facet": "^0.1.32", "@flanksource/icons": "^1.0.53", "@iconify-json/carbon": "^1.2.0", "@iconify-json/fluent": "^1.2.0", From f1cc0d33f5005f3902f69ef749a0d47aaccff90e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Wed, 15 Apr 2026 16:41:52 +0300 Subject: [PATCH 43/48] refactor(auth/oidc,db): refactor MFA filter logic and mount OAuth routes before OIDC catch-all Replace if-else chain with switch statement for MFA filter comparison in GetAccessLogsForUIRef for improved readability and maintainability. Mount OAuth 2.0 Protected Resource Metadata endpoints (RFC 9728) before the OIDC provider catch-all route to ensure they take precedence over /.well-known/* routes. Update go.sum with additional Google Cloud and dependency entries. --- auth/oidc/routes.go | 4 + db/applications.go | 5 +- go.sum | 415 ++++++++++++++++++++++++++++ tests/oidc_e2e/oidc-signing-key.pem | 27 -- 4 files changed, 422 insertions(+), 29 deletions(-) delete mode 100644 tests/oidc_e2e/oidc-signing-key.pem diff --git a/auth/oidc/routes.go b/auth/oidc/routes.go index 280131584..3e5777cda 100644 --- a/auth/oidc/routes.go +++ b/auth/oidc/routes.go @@ -27,6 +27,10 @@ func MountRoutes(e *echo.Echo, ctx context.Context, issuerURL, signingKeyPath st e.GET("/oidc/login", loginHandler.ShowForm) e.POST("/oidc/login", loginHandler.HandleSubmit) + // RFC 9728 OAuth 2.0 Protected Resource Metadata endpoints — registered before + // the OIDC provider catch-all so they take precedence over /.well-known/*. + mountOAuthRoutes(e, oidcIssuer) + // Standard OIDC protocol endpoints — mounted at the root so that the issuer URL // and the authorization_endpoint/token_endpoint values in the discovery document // resolve to real routes on this server. diff --git a/db/applications.go b/db/applications.go index 1c4fe4e22..cb475bc05 100644 --- a/db/applications.go +++ b/db/applications.go @@ -672,9 +672,10 @@ func GetAccessLogsForUIRef(ctx context.Context, filters *api.AccessLogsUIFilters } } - if filters.MFA == "true" { + switch filters.MFA { + case "true": q = q.Where("config_access_logs.mfa = true") - } else if filters.MFA == "false" { + case "false": q = q.Where("config_access_logs.mfa = false") } diff --git a/go.sum b/go.sum index ad0f8c72e..16dd329fd 100644 --- a/go.sum +++ b/go.sum @@ -19,37 +19,137 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/accessapproval v1.8.8/go.mod h1:RFwPY9JDKseP4gJrX1BlAVsP5O6kI8NdGlTmaeDefmk= +cloud.google.com/go/accesscontextmanager v1.9.7/go.mod h1:i6e0nd5CPcrh7+YwGq4bKvju5YB9sgoAip+mXU73aMM= +cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo= +cloud.google.com/go/aiplatform v1.113.0/go.mod h1:B8fcWtC2vSadapIQqweJrTATJe/odNDjk2uIA5kmXog= +cloud.google.com/go/alloydb v1.14.0/go.mod h1:OTBY1HoL0Z8PsHoMMVhkaUPKyY8oP7hzIAe/Dna6UHk= +cloud.google.com/go/alloydbconn v1.13.2/go.mod h1:0wlYQAOr2XuvxYsvNNVckmG2v17WVUKzMD+gmTOibSU= +cloud.google.com/go/analytics v0.30.1/go.mod h1:V/FnINU5kMOsttZnKPnXfKi6clJUHTEXUKQjHxcNK8A= +cloud.google.com/go/apigateway v1.7.7/go.mod h1:j1bCmrUK1BzVHpiIyTApxB7cRyhivKzltqLmp6j6i7U= +cloud.google.com/go/apigeeconnect v1.7.7/go.mod h1:ftGK3nca0JePiVLl0A6alaMjKdOc5C+sAkFMyH2RH8U= +cloud.google.com/go/apigeeregistry v0.10.0/go.mod h1:SAlF5OhKvyLDuwWAaFAIVJjrEqKRrGTPkJs+TWNnSqg= +cloud.google.com/go/appengine v1.9.7/go.mod h1:y1XpGVeAhbsNzHida79cHbr3pFRsym0ob8xnC8yphbo= +cloud.google.com/go/area120 v0.9.7/go.mod h1:5nJ0yksmjOMfc4Zpk+okWfJ3A1004FvB82rfia+ZLaY= +cloud.google.com/go/artifactregistry v1.19.0/go.mod h1:UEAPCgHDFC1q+A8nnVxXHPEy9KCVOeavFBF1fEChQvU= +cloud.google.com/go/asset v1.22.0/go.mod h1:q80JP2TeWWzMCazYnrAfDf36aQKf1QiKzzpNLflJwf8= +cloud.google.com/go/assuredworkloads v1.13.0/go.mod h1:o/oHEOnUlribR+uJWTKQo8A5RhSl9K9FNeMOew4TJ3M= cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/automl v1.15.0/go.mod h1:U9zOtQb8zVrFNGTuW3BfxeqmLyeleLgT9B12EaXfODg= +cloud.google.com/go/baremetalsolution v1.4.0/go.mod h1:K6C6g4aS8LW95I0fEHZiBsBlh0UxwDLGf+S/vyfXbvg= +cloud.google.com/go/batch v1.14.0/go.mod h1:oeQveyG6NDS/ks2ilOP4LzKRmuIaI7GLe0CkR7WF6pk= +cloud.google.com/go/beyondcorp v1.2.0/go.mod h1:sszcgxpPPBEfLzbI0aYCTg6tT1tyt3CmKav3NZIUcvI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ= +cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI= +cloud.google.com/go/billing v1.21.0/go.mod h1:ZGairB3EVnb3i09E2SxFxo50p5unPaMTuo1jh6jW9js= +cloud.google.com/go/binaryauthorization v1.10.0/go.mod h1:WOuiaQkI4PU/okwrcREjSAr2AUtjQgVe+PlrXKOmKKw= +cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo= +cloud.google.com/go/channel v1.21.0/go.mod h1:8v3TwHtgLmFxTpL2U+e10CLFOQN8u/Vr9RhYcJUS3y8= +cloud.google.com/go/cloudbuild v1.25.0/go.mod h1:lCu+T6IPkobPo2Nw+vCE7wuaAl9HbXLzdPx/tcF+oWo= +cloud.google.com/go/clouddms v1.8.8/go.mod h1:QtCyw+a73dlkDb2q20aTAPvfaTZCepDDi6Gb1AKq0a4= cloud.google.com/go/cloudsqlconn v1.20.0 h1:5EBr98dktt5QStX6jacFTECTQ4rxfY6qpIUIV9YNRqo= cloud.google.com/go/cloudsqlconn v1.20.0/go.mod h1:YCoWR0SWYTDf9npeqq8ODFN1WdGMGVC5G74+A3CXXP4= +cloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4= +cloud.google.com/go/compute v1.53.0/go.mod h1:zdogTa7daHhEtEX92+S5IARtQmi/RNVPUfoI8Jhl8Do= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/contactcenterinsights v1.17.4/go.mod h1:kZe6yOnKDfpPz2GphDHynxk/Spx+53UX/pGf+SmWAKM= +cloud.google.com/go/container v1.45.0/go.mod h1:eB6jUfJLjne9VsTDGcH7mnj6JyZK+KOUIA6KZnYE/ds= +cloud.google.com/go/containeranalysis v0.14.2/go.mod h1:FjppROiUtP9cyMegdWdY/TsBSGc6kqh1GjA2NOJXXL8= +cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= +cloud.google.com/go/dataflow v0.11.1/go.mod h1:3s6y/h5Qz7uuxTmKJKBifkYZ3zs63jS+6VGtSu8Cf7Y= +cloud.google.com/go/dataform v0.12.1/go.mod h1:atGS8ReRjfNDUQib0X/o/7Gi2bqHI2G7/J86LKiGimE= +cloud.google.com/go/datafusion v1.8.7/go.mod h1:4dkFb1la41qCEXh1AzYtFwl842bu2ikTUXyKhjvFCb0= +cloud.google.com/go/datalabeling v0.9.7/go.mod h1:EEUVn+wNn3jl19P2S13FqE1s9LsKzRsPuuMRq2CMsOk= +cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA= +cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8= +cloud.google.com/go/dataqna v0.9.8/go.mod h1:2lHKmGPOqzzuqCc5NI0+Xrd5om4ulxGwPpLB4AnFgpA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY= +cloud.google.com/go/datastream v1.15.1/go.mod h1:aV1Grr9LFon0YvqryE5/gF1XAhcau2uxN2OvQJPpqRw= +cloud.google.com/go/deploy v1.27.3/go.mod h1:7LFIYYTSSdljYRqY3n+JSmIFdD4lv6aMD5xg0crB5iw= +cloud.google.com/go/dialogflow v1.73.0/go.mod h1:vFkeDO7ishnfakWVLlbgIynQGTFJ/YaVMlYmSn5M+1o= +cloud.google.com/go/dlp v1.28.0/go.mod h1:C3od1fIK8lf7Kr62aU1Uh0z4OL5Z8s3do3znAiEupAw= +cloud.google.com/go/documentai v1.39.0/go.mod h1:KmlLO93F7GRU8dENXRxvt+7V8o7eCG6Y6WDitKbcYJs= +cloud.google.com/go/domains v0.10.7/go.mod h1:T3WG/QUAO/52z4tUPooKS8AY7yXaFxPYn1V3F0/JbNQ= +cloud.google.com/go/edgecontainer v1.4.4/go.mod h1:yyNVHsCKtsX/0mqFdbljQw0Uo660q2dlMPaiqYiC2Tg= +cloud.google.com/go/errorreporting v0.4.0/go.mod h1:dZGEhqzdHZSRxxWLVjC3Ue5CVaROzvP58D9rU6zbBfw= +cloud.google.com/go/essentialcontacts v1.7.7/go.mod h1:ytycWAEn/aKUMRKQPMVgMrAtphEMgjbzL8vFwM3tqXs= +cloud.google.com/go/eventarc v1.18.0/go.mod h1:/6SDoqh5+9QNUqCX4/oQcJVK16fG/snHBSXu7lrJtO8= +cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac= +cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= +cloud.google.com/go/functions v1.19.7/go.mod h1:xbcKfS7GoIcaXr2FSwmtn9NXal1JR4TV6iYZlgXffwA= +cloud.google.com/go/gkebackup v1.8.1/go.mod h1:GAaAl+O5D9uISH5MnClUop2esQW4pDa2qe/95A4l7YQ= +cloud.google.com/go/gkeconnect v0.12.5/go.mod h1:wMD2RXcsAWlkREZWJDVeDV70PYka1iEb9stFmgpw+5o= +cloud.google.com/go/gkehub v0.16.0/go.mod h1:ADp27Ucor8v81wY+x/5pOxTorxkPj/xswH3AUpN62GU= +cloud.google.com/go/gkemulticloud v1.6.0/go.mod h1:bGpd4o/Z5Z/XFlaojkgdVisHRwb+fLJvUPzsmV0I9ok= +cloud.google.com/go/gsuiteaddons v1.7.8/go.mod h1:DBKNHH4YXAdd/rd6zVvtOGAJNGo0ekOh+nIjTUDEJ5U= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/iap v1.11.3/go.mod h1:+gXO0ClH62k2LVlfhHzrpiHQNyINlEVmGAE3+DB4ShU= +cloud.google.com/go/ids v1.5.7/go.mod h1:N3ZQOIgIBwwOu2tzyhmh3JDT+kt8PcoKkn2BRT9Qe4A= +cloud.google.com/go/iot v1.8.7/go.mod h1:HvVcypV8LPv1yTXSLCNK+YCtqGHhq+p0F3BXETfpN+U= cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/language v1.14.6/go.mod h1:7y3J9OexQsfkWNGCxhT+7lb64pa60e12ZCoWDOHxJ1M= +cloud.google.com/go/lifesciences v0.10.7/go.mod h1:v3AbTki9iWttEls/Wf4ag3EqeLRHofploOcpsLnu7iY= cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/managedidentities v1.7.7/go.mod h1:nwNlMxtBo2YJMvsKXRtAD1bL41qiCI9npS7cbqrsJUs= +cloud.google.com/go/maps v1.26.0/go.mod h1:+auempdONAP8emtm48aCfNo1ZC+3CJniRA1h8J4u7bY= +cloud.google.com/go/mediatranslation v0.9.7/go.mod h1:mz3v6PR7+Fd/1bYrRxNFGnd+p4wqdc/fyutqC5QHctw= +cloud.google.com/go/memcache v1.11.7/go.mod h1:AU1jYlUqCihxapcJ1GGMtlMWDVhzjbfUWBXqsXa4rBg= +cloud.google.com/go/metastore v1.14.8/go.mod h1:h1XI2LpD4ohJhQYn9TwXqKb5sVt6KSo47ft96SiFF1s= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/networkconnectivity v1.19.1/go.mod h1:Q5v6uNNNz8BP232uuXM66XgWML9m379xhwv58Y+8Kb0= +cloud.google.com/go/networkmanagement v1.21.0/go.mod h1:clG/5Yt0wQ57qSH6Yh7oehQYlobHw3F6nb3Pn4ig5hU= +cloud.google.com/go/networksecurity v0.11.0/go.mod h1:JLgDsg4tOyJ3eMO8lypjqMftbfd60SJ+P7T+DUmWBsM= +cloud.google.com/go/notebooks v1.12.7/go.mod h1:uR9pxAkKmlNloibMr9Q1t8WhIu4P2JeqJs7c064/0Mo= +cloud.google.com/go/optimization v1.7.7/go.mod h1:OY2IAlX23o52qwMAZ0w65wibKuV12a4x6IHDTCq6kcU= +cloud.google.com/go/orchestration v1.11.10/go.mod h1:tz7m1s4wNEvhNNIM3JOMH0lYxBssu9+7si5MCPw/4/0= +cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0= +cloud.google.com/go/osconfig v1.15.1/go.mod h1:NegylQQl0+5m+I+4Ey/g3HGeQxKkncQ1q+Il4DZ8PME= +cloud.google.com/go/oslogin v1.14.7/go.mod h1:NB6NqBHfDMwznePdBVX+ILllc1oPCdNSGp5u/WIyndY= +cloud.google.com/go/phishingprotection v0.9.7/go.mod h1:JTI4HNGyAbWolBoNOoCyCF0e3cqPNrYnlievHU49EwE= +cloud.google.com/go/policytroubleshooter v1.11.7/go.mod h1:JP/aQ+bUkt4Gz6lQXBi/+A/6nyNRZ0Pvxui5Xl9ieyk= +cloud.google.com/go/privatecatalog v0.10.8/go.mod h1:BkLHi+rtAGYBt5DocXLytHhF0n6F03Tegxgty40Y7aA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk= +cloud.google.com/go/pubsub/v2 v2.3.0/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw= +cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= +cloud.google.com/go/recaptchaenterprise/v2 v2.21.0/go.mod h1:HxQYqZC2/zl2CvKN7jJEv71vEdDi1GMGNUiZxnpiuVI= +cloud.google.com/go/recommendationengine v0.9.7/go.mod h1:snZ/FL147u86Jqpv1j95R+CyU5NvL/UzYiyDo6UByTM= +cloud.google.com/go/recommender v1.13.6/go.mod h1:y5/5womtdOaIM3xx+76vbsiA+8EBTIVfWnxHDFHBGJM= +cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4= +cloud.google.com/go/resourcemanager v1.10.7/go.mod h1:rScGkr6j2eFwxAjctvOP/8sqnEpDbQ9r5CKwKfomqjs= +cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= +cloud.google.com/go/retail v1.25.1/go.mod h1:J75G8pd+DH0SHueL9IJw7Y5d2VhTsjFsk+F1t9f8jXc= +cloud.google.com/go/run v1.14.0/go.mod h1:KStBOpjX7m47Yi1xStWSkvJcCqLr+PMUkz6p3po5/VA= +cloud.google.com/go/scheduler v1.11.8/go.mod h1:bNKU7/f04eoM6iKQpwVLvFNBgGyJNS87RiFN73mIPik= +cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q= +cloud.google.com/go/security v1.19.2/go.mod h1:KXmf64mnOsLVKe8mk/bZpU1Rsvxqc0Ej0A6tgCeN93w= +cloud.google.com/go/securitycenter v1.38.1/go.mod h1:Ge2D/SlG2lP1FrQD7wXHy8qyeloRenvKXeB4e7zO6z0= +cloud.google.com/go/servicedirectory v1.12.7/go.mod h1:gOtN+qbuCMH6tj2dqlDY3qQL7w3V0+nkWaZElnJK8Ps= +cloud.google.com/go/shell v1.8.7/go.mod h1:OTke7qc3laNEW5Jr5OV9VR3IwU5x5VqGOE6705zFex4= +cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw= +cloud.google.com/go/speech v1.29.0/go.mod h1:wtUmIS/h0ZYU6cPA9klcyST3f6i2FdnvNDqENjrRDds= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -57,10 +157,29 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58= cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y= +cloud.google.com/go/talent v1.8.4/go.mod h1:3yukBXUTVFNyKcJpUExW/k5gqEy8qW6OCNj7WdN0MWo= +cloud.google.com/go/texttospeech v1.16.0/go.mod h1:AeSkoH3ziPvapsuyI07TWY4oGxluAjntX+pF4PJ2jy0= +cloud.google.com/go/tpu v1.8.4/go.mod h1:ul0cyWSHr6jHGZYElZe6HvQn35VY93RAlwpDiSBRnPA= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +cloud.google.com/go/translate v1.12.7/go.mod h1:wwJp14NZyWvcrFANhIXutXj0pOBkYciBHwSlUOykcjI= +cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= +cloud.google.com/go/video v1.27.1/go.mod h1:xzfAC77B4vtnbi/TT3UUxEjCa/+Ehy5EA8w470ytOig= +cloud.google.com/go/videointelligence v1.12.7/go.mod h1:XAk5hCMY+GihxJ55jNoMdwdXSNZnCl3wGs2+94gK7MA= +cloud.google.com/go/vision/v2 v2.9.6/go.mod h1:lJC+vP15D5znJvHQYjEoTKnpToX1L93BUlvBmzM0gyg= +cloud.google.com/go/vmmigration v1.10.0/go.mod h1:LDztCWEb+RwS1bPg4Xzt0fcJS9kVrFxa3ejhH7OW9vg= +cloud.google.com/go/vmwareengine v1.3.6/go.mod h1:ps0rb+Skgpt9ppHYC0o5DqtJ5ld2FyS8sAqtbHH8t9s= +cloud.google.com/go/vpcaccess v1.8.7/go.mod h1:9RYw5bVvk4Z51Rc8vwXT63yjEiMD/l7XyEaDyrNHgmk= +cloud.google.com/go/webrisk v1.11.2/go.mod h1:yH44GeXz5iz4HFsIlGeoVvnjwnmfbni7Lwj1SelV4f0= +cloud.google.com/go/websecurityscanner v1.7.7/go.mod h1:ng/PzARaus3Bj4Os4LpUnyYHsbtJky1HbBDmz148v1o= +cloud.google.com/go/workflows v1.14.3/go.mod h1:CC9+YdVI2Kvp0L58WajHpEfKJxhrtRh3uQ0SYWcmAk4= code.gitea.io/sdk/gitea v0.14.0 h1:m4J352I3p9+bmJUfS+g0odeQzBY/5OXP91Gv6D4fnJ0= code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -68,6 +187,10 @@ filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= fortio.org/safecast v1.2.0 h1:ckQJNenMJHycqPsi/QrzA4EUX5WQkyd+hGO4mxt/a8w= fortio.org/safecast v1.2.0/go.mod h1:xZmcPk3vi4kuUFf+tq4SvnlVdwViqf6ZSZl91Jr9Jdg= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AssemblyAI/assemblyai-go-sdk v1.3.0/go.mod h1:H0naZbvpIW49cDA5ZZ/gggeXqi7ojSGB1mqshRk6kNE= +github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= @@ -88,6 +211,9 @@ github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 h1:m/sWOGCREuSBqg2 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0/go.mod h1:Pu5Zksi2KrU7LPbZbNINx6fuVrUp/ffvpxdDj+i8LeE= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= +github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.9.1/go.mod h1:NydgUaroiShkgOcb+X6OUdS3RalWBrvDNtOyFHJtsZY= +github.com/Azure/azure-sdk-for-go/sdk/monitor/azquery v1.2.0/go.mod h1:oI5SPI1vpNJYfP9MPWXthq7jDfh9xTAuQVBKPOu7DPo= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= @@ -98,6 +224,11 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuo github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew= +github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= @@ -106,25 +237,35 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgv github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Code-Hex/go-generics-cache v1.3.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.8/go.mod h1:exon/I6I+5u/ab7AHmGh0eCXGoYZO5cjqA3wHJlYFFQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0/go.mod h1:rKOFVIPbNs2wZeh7ZeQ0D9p/XLgbNiTr5m7x6KuAshk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0/go.mod h1:dtCRwgvytbGKWdlrjMOg9geBoRwRpCYWIOM/JhVsDIc= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko= +github.com/IBM/watsonx-go v1.0.0/go.mod h1:8lzvpe/158JkrzvcoIcIj6OdNty5iC9co5nQHfkhRtM= +github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= @@ -134,17 +275,22 @@ github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4Oe github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/RaveNoX/go-jsonmerge v1.0.0 h1:2e0nqnadoGUP8rAvcA0hkQelZreVO5X3BHomT2XMrAk= github.com/RaveNoX/go-jsonmerge v1.0.0/go.mod h1:qYM/NA77LhO4h51JJM7Z+xBU3ovqrNIACZe+SkSNVFo= github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU= github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4= +github.com/Snawoot/go-http-digest-auth-client v1.1.3/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg= github.com/TomOnTime/utfutil v1.0.0 h1:/0Ivgo2OjXJxo8i7zgvs7ewSFZMLwCRGm3P5Umowb90= github.com/TomOnTime/utfutil v1.0.0/go.mod h1:l9lZmOniizVSuIliSkEf87qivMRlSNzbdBFKjuLRg1c= +github.com/Venafi/vcert/v5 v5.12.2/go.mod h1:x3l0pB0q0E6wuhPe7nzfkUEwwraK7amnBWQ4LtT1bbw= github.com/WinterYukky/gorm-extra-clause-plugin v0.4.0 h1:e4gYsN9tNzoBMYKYBaGwwZpSljJhW231+1cBlYwv8YQ= github.com/WinterYukky/gorm-extra-clause-plugin v0.4.0/go.mod h1:jNWq8AymgsVev9Kq6mke0b3o3yzY6bTSwjMDfTvZPPM= +github.com/XSAM/otelsql v0.39.0/go.mod h1:uMOXLUX+wkuAuP0AR3B45NXX7E9lJS2mERa8gqdU8R0= github.com/adityathebe/go-strip-markdown/v2 v2.0.1 h1:/Dxr9Rnn6h8VIwh2rqpYTUyoN4Hx4SXeEOjrz+JUO6I= github.com/adityathebe/go-strip-markdown/v2 v2.0.1/go.mod h1:Ze3XxKLEV5u8VWBaiAALVKOIA7uLZghVIUvQrICHFV0= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= @@ -155,12 +301,14 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akamai/AkamaiOPEN-edgegrid-golang/v12 v12.0.0/go.mod h1:Bf6hnZkloZnfL4I/gFGnMMMdMHiu/ERnSOWtFgnodDk= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -169,23 +317,33 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/amikos-tech/chroma-go v0.1.4/go.mod h1:sT6uXOo/L5S/Q0v9jpYtoR1iOM68hUE2itWw8sOwLHY= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= github.com/antchfx/xmlquery v1.5.1 h1:T9I4Ns1EXiWHy0IqKupGhnfTQtJwlGrpXtauYOoNv78= github.com/antchfx/xmlquery v1.5.1/go.mod h1:bVqnl7TaDXSReKINrhZz+2E/PbCu2tUahb+wZ7WZNT8= github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI= github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= @@ -197,9 +355,12 @@ github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87/go.mod h1:ZeQC4gVarhdcWeM1c90DyBLaBCNhEeAbKUXwVI/byvw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.13/go.mod h1:RxLhhGmjEidlLTRZyk1BLMigHONURhQakw2//prq+DA= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6 h1:xuOfOJR0SPBrHhzAXZ5c+8i1KyJ+aUVJ2cl8DT16qH4= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.6/go.mod h1:rUVOV4y5upo55JxPss99p9FaN9BvqUjFgE/N54tvLuE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= @@ -213,14 +374,20 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y= +github.com/aws/aws-sdk-go-v2/service/bedrockagent v1.40.0/go.mod h1:WlMBqEPeaBywfaXoMAfpitHvwezq555o8waYL3cCPqo= +github.com/aws/aws-sdk-go-v2/service/bedrockagentruntime v1.41.0/go.mod h1:Kek1IWlEDT1bp8kO+soWZh37Cb13LppHUTbMiJunna0= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.24.3/go.mod h1:PKGlRhLmSZuA6iCbRD1oZKrTJHdm6NWwWBvHxfDNHTA= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0 h1:3yaFbUbuLfN8n1q01wZtQtHRzUDc/jm0VvniMY0IPE8= github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.65.0/go.mod h1:PobeppEnIjw4pcgjFryNDZCTH7AiqZw0yb5r98Gvf9c= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= github.com/aws/aws-sdk-go-v2/service/eks v1.77.0 h1:Z5mTpmbJKU7jEM7xoXI5tO4Nm0JUZSgVSFkpYuu6Ic0= github.com/aws/aws-sdk-go-v2/service/eks v1.77.0/go.mod h1:Qg678m+87sCuJhcsZojenz8mblYG+Tq86V4m3hjVz0s= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= @@ -228,10 +395,15 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWUR github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ= github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= +github.com/aws/aws-sdk-go-v2/service/route53 v1.58.4/go.mod h1:xNLZLn4SusktBQ5moqUOgiDKGz3a7vHwF4W0KD+WBPc= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.7/go.mod h1:1X1NotbcGHH7PCQJ98PsExSxsJj/VWzz8MfFz43+02M= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.11/go.mod h1:hdZDKzao0PBfJJygT7T92x2uVcWc/htqlhrjFIjnHDM= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.21/go.mod h1:t98Ssq+qtXKXl2SFtaSkuT6X42FSM//fnO6sfq5RqGM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1/go.mod h1:IyVabkWrs8SNdOEZLyFFcW9bUltV4G6OQS0s6H20PHg= github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= @@ -248,12 +420,16 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -268,6 +444,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/gorm-adapter/v3 v3.39.0 h1:k15txH6vE4796MuA+LFcU8I1vMjutklyzMXfjDz7lzo= @@ -277,6 +455,8 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -293,6 +473,7 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -323,12 +504,16 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= github.com/clarkmcc/gorm-sqlite v0.0.0-20240426202654-00ed082c0311 h1://GDWpsQ8pSg8u8SCavanukwPu5yE0Rz3uu7CuFVfFc= github.com/clarkmcc/gorm-sqlite v0.0.0-20240426202654-00ed082c0311/go.mod h1:HrR53jwmQF7sTyNxEJ3rqfx9sRVnaTUqIo1nXn0KRho= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= @@ -338,10 +523,23 @@ github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94 h1:kkHPnzHm5Ln7WA0XYjr github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/cohere-ai/tokenizer v1.1.2/go.mod h1:9MNFPd9j1fuiEK3ua2HSCUxxcrfGMlSqpa93livg/C0= +github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -355,24 +553,34 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/digitalocean/godo v1.165.1/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/eko/gocache/lib/v4 v4.2.3 h1:s78TFqEGAH3SbzP4N40D755JYT/aaGFKEPrsUtC1chU= github.com/eko/gocache/lib/v4 v4.2.3/go.mod h1:Zus8mwmaPu1VYOzfomb+Dvx2wV7fT5jDRbHYtQM6MEY= github.com/eko/gocache/store/go_cache/v4 v4.2.4 h1:toHpoIi4HhuXYv1bFOh5FiEQhpli4sWoSAN74j3/MXw= github.com/eko/gocache/store/go_cache/v4 v4.2.4/go.mod h1:oZcTjIjtHiCKCFS5KfxFrcmHFJKJd3wCNwuYeqWBuhI= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/eliben/go-sentencepiece v0.6.0/go.mod h1:nNYk4aMzgBoI6QFp4LUG8Eu1uO9fHD9L5ZEre93o9+c= github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= @@ -417,6 +625,7 @@ github.com/flanksource/clicky v1.21.1 h1:Vk/q39QFCp+BEvX+mVfoeyfVP4IvLs18DlI5gMB github.com/flanksource/clicky v1.21.1/go.mod h1:Wg30x88982ejUzKqtw+Sm7UMRHTB8bnIQBXHi9s54RU= github.com/flanksource/commons v1.50.2 h1:P3xLuIwc/GaNqDX1NSZrvm1ktX52FvQjpPYoGRgz+ko= github.com/flanksource/commons v1.50.2/go.mod h1:m+so9LQqb04hkRlV6iza3BMPIDu6EpC5W2izP4MK3Bw= +github.com/flanksource/commons-db v0.1.1/go.mod h1:d627xQrhgKSZ/DLBSmrF64LxnuamTvTWTGg+5oyJJpw= github.com/flanksource/commons-test v0.1.13 h1:DLb3q1a7d+BpfxZDy2bdY8ZA5z1+UTYFEO9DYd15bw4= github.com/flanksource/commons-test v0.1.13/go.mod h1:T0zLA9F55jlaOhtvjj1Ot7QZQhtn2baAuflT+27ueG8= github.com/flanksource/deps v1.0.24 h1:X23SZb2nxCDsS1wRiuqyvUYpA3KQxcQR9YfB8H/oTgo= @@ -431,12 +640,14 @@ github.com/flanksource/kopper v1.0.21 h1:hbmwbEYcZp1zMfdtjsiGQJfAqp/sjY47gWypp2B github.com/flanksource/kopper v1.0.21/go.mod h1:1xAGxHUkaS8DcXQ1dRsTMriTXEUbqz+VvLYknbvWVRg= github.com/flanksource/kubectl-neat v1.0.4 h1:t5/9CqgE84oEtB0KitgJ2+WIeLfD+RhXSxYrqb4X8yI= github.com/flanksource/kubectl-neat v1.0.4/go.mod h1:Un/Voyh3cmiZNKQrW/TkAl28nAA7vwnwDGVjRErKjOw= +github.com/flanksource/maroto/v2 v2.4.2/go.mod h1:2ox4ZhXbIY+1fyJwuXAca0S/05soOlFSWqyqCnSPtO4= github.com/flanksource/sandbox-runtime v1.0.1 h1:zBzNx9GoZILo1ot4qI2wd/gqny0vejvex3xnJzsmvgE= github.com/flanksource/sandbox-runtime v1.0.1/go.mod h1:HCeOqw4QQOpvzDeN3hMdQpxIZ9yrp0/5ziXjiiOw5ec= github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo= github.com/fluxcd/pkg/gittestserver v0.21.0 h1:2ez/cCGbGHz/Rp1IIbjqRsuTDgMmW98or3+8cSWpbHk= github.com/fluxcd/pkg/gittestserver v0.21.0/go.mod h1:KbTkLjhjHnVbepN4d3OWo6T+nQMFU+lZgrTUm3vIHgo= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -445,9 +656,12 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gage-technologies/mistral-go v1.1.0/go.mod h1:tF++Xt7U975GcLlzhrjSQb8l/x+PrriO9QEdsgm9l28= github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64= github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= +github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA= +github.com/getzep/zep-go v1.0.4/go.mod h1:HC1Gz7oiyrzOTvzeKC4dQKUiUy87zpIJl0ZFXXdHuss= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= @@ -456,8 +670,10 @@ github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZ github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -471,6 +687,7 @@ github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQq github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= @@ -482,6 +699,7 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -496,12 +714,18 @@ github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA= github.com/go-openapi/inflect v0.21.5/go.mod h1:GypUyi6bU880NYurWaEH2CmH84zFDNd+EhhmzroHmB4= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.24.2/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= @@ -532,6 +756,8 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxE github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= @@ -539,6 +765,7 @@ github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI6 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= @@ -553,16 +780,21 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/goccmack/gocc v1.0.2/go.mod h1:LXX2tFVUggS/Zgx/ICPOr3MLyusuM7EcbfkPvNsjdO8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0= github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -575,7 +807,9 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -610,6 +844,7 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -618,6 +853,9 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -635,8 +873,10 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= +github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= @@ -665,6 +905,7 @@ github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmI github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -677,10 +918,14 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= @@ -689,14 +934,30 @@ github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6 github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf h1:I1sbT4ZbIt9i+hB1zfKw2mE8C12TuGxPiW7YmtLbPa4= github.com/hairyhenderson/toml v0.4.2-0.20210923231440-40456b8e66cf/go.mod h1:jDHmWDKZY6MIIYltYYfW4Rs7hQ50oS4qf/6spSiZAxY= github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce h1:cVkYhlWAxwuS2/Yp6qPtcl0fGpcWxuZNonywHZ6/I+s= github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce/go.mod h1:7TyiGlHI+IO+iJbqRZ82QbFtvgj/AIcFm5qc9DLn7Kc= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -707,19 +968,27 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/hashicorp/vault/sdk v0.20.0/go.mod h1:xEjAt/n/2lHBAkYiRPRmvf1d5B6HlisPh2pELlRCosk= github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI= github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/itchyny/go-yaml v0.0.0-20251001235044-fca9a0999f15/go.mod h1:Tmbz8uw5I/I6NvVpEGuhzlElCGS5hPoXJkt7l+ul6LE= github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= @@ -794,8 +1063,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jenkins-x/go-scm v1.15.16 h1:fdmMcjlA+VOpWO1lS8V7jzxIGvwgJ6Ls286FUpHoUSk= github.com/jenkins-x/go-scm v1.15.16/go.mod h1:RU3n2g3nxbIkjjm7cg7iOUh/7Wr1V+bTr/YM8qZeAr0= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -805,6 +1076,8 @@ github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -820,6 +1093,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= @@ -874,6 +1149,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= @@ -900,21 +1177,38 @@ github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSY github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/metaphorsystems/metaphor-go v0.0.0-20230816231421-43794c04824e/go.mod h1:mDz8kHE7x6Ja95drCQ2T1vLyPRc/t69Cf3wau91E3QU= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw= github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= +github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/milvus-io/milvus-proto/go-api/v2 v2.6.1-0.20250819024338-07695f709619/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= +github.com/milvus-io/milvus-sdk-go/v2 v2.4.0/go.mod h1:8IKyxVV+kd+RADMuMpo8GXnTDq5ZxrSSWpe9nJieboQ= +github.com/milvus-io/milvus/client/v2 v2.6.0/go.mod h1:5ppFKT61Fh5Z1MkAhK7+nLnlh9C+ENBe/dpgFBH0te0= +github.com/milvus-io/milvus/pkg/v2 v2.0.0-20250319085209-5a6b4e56d59e/go.mod h1:37AWzxVs2NS4QUJrkcbeLUwi+4Av0h5mEdjLI62EANU= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -925,6 +1219,7 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -942,10 +1237,21 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.14/go.mod h1:seG5UKwYdZXb7M1y1vvu53mNh3xq2B6um/XUgYAgvkM= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/nlpodyssey/cybertron v0.2.1/go.mod h1:Vg9PeB8EkOTAgSKQ68B3hhKUGmB6Vs734dBdCyE4SVM= +github.com/nlpodyssey/gopickle v0.2.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY= +github.com/nlpodyssey/gotokenizers v0.2.0/go.mod h1:SBLbuSQhpni9M7U+Ie6O46TXYN73T2Cuw/4eeYHYJ+s= +github.com/nlpodyssey/spago v1.1.0/go.mod h1:jDWGZwrB4B61U6Tf3/+MVlWOtNsk3EUA7G13UDHlnjQ= +github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/ohler55/ojg v1.28.0 h1:8xClBgMIRRJGDUC9xNe7NprP4kD2C3mQMeon3wY4KXA= github.com/ohler55/ojg v1.28.0/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= @@ -956,25 +1262,38 @@ github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4= github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc= github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ= github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s= github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/ory/client-go v1.22.8 h1:v1NCqmKIKFQFYQihWUJuksvxILujvVA8HUOCAs5Lxr0= github.com/ory/client-go v1.22.8/go.mod h1:o/1hF5MKq3gyn9nUWZF/VVz35nitCzsGfIwl5SXVJ1Y= +github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pgvector/pgvector-go v0.1.1/go.mod h1:wLJgD/ODkdtd2LJK4l6evHXTuG+8PxymYAVomKHOWac= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pinecone-io/go-pinecone v0.4.1/go.mod h1:KwWSueZFx9zccC+thBk13+LDiOgii8cff9bliUI4tQs= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= @@ -1036,8 +1355,10 @@ github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/redis/rueidis v1.0.34/go.mod h1:g8nPmgR4C68N3abFiOc/gUOSEKw3Tom6/teYMehg4RE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= @@ -1053,6 +1374,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -1064,7 +1386,11 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samber/oops v1.21.0 h1:18atcO4oEigNFuGXqr3NZWZ6P0XOSEXyBSAMXdQRxTc= @@ -1080,6 +1406,7 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0= github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= @@ -1103,16 +1430,23 @@ github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28Jjd github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -1133,6 +1467,18 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= +github.com/testcontainers/testcontainers-go/modules/chroma v0.37.0/go.mod h1:IWJavzQy7rxM40OqOgSN5iyckgAw21wDyE+NhSctatk= +github.com/testcontainers/testcontainers-go/modules/mariadb v0.38.0/go.mod h1:26mrWngnaRhxmgy942aVfUihLnihbIGsuIds6gGBnIE= +github.com/testcontainers/testcontainers-go/modules/milvus v0.37.0/go.mod h1:bCdLqxjPKax120BMl4aO/A0gs9+4FeJkLBVf9WpjFoQ= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.37.0/go.mod h1:e9/4dGJfSZW59/kXGf/ksrEvA+BqP/daax0Usp2cpsM= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc= +github.com/testcontainers/testcontainers-go/modules/opensearch v0.37.0/go.mod h1:2jEljlB96QHSHF7Vo9S8zEDisPPrfsddzSvsCR1ihNQ= +github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0/go.mod h1:Qj/eGbRbO/rEYdcRLmN+bEojzatP/+NS1y8ojl2PQsc= +github.com/testcontainers/testcontainers-go/modules/redis v0.37.0/go.mod h1:Abu9g/25Qv+FkYVx3U4Voaynou1c+7D0HIhaQJXvk6E= +github.com/testcontainers/testcontainers-go/modules/weaviate v0.37.0/go.mod h1:VdjCqOCJGzlGLS2p4NdLjN5rqN3/53mle+Gb+irCbOE= github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU= github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -1158,8 +1504,11 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc= github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= @@ -1175,12 +1524,20 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/weaviate/weaviate v1.29.0/go.mod h1:UsnbM1Kmm5Om+UPU6DTo421SDeMD8SqCJqsBs/nwgcI= +github.com/weaviate/weaviate-go-client/v5 v5.0.2/go.mod h1:CwZehIL4s3VfkzTu12Wy8VAUtELRtQFUt2ZniBF/lQM= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1192,6 +1549,8 @@ github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= @@ -1200,8 +1559,10 @@ github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzx github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1217,6 +1578,9 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCTOi1M= github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= @@ -1224,6 +1588,22 @@ github.com/zitadel/oidc/v3 v3.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrD github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= +gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow= +gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= +gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M= +gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw= +gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1233,6 +1613,7 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0/go.mod h1:gs3y8jvJscW5D+FzrZvJZEsGj+xlMCF0S1x4R6ktiNo= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 h1:6YeICKmGrvgJ5th4+OMNpcuoB6q/Xs8gt0YCO7MUv1k= @@ -1241,10 +1622,12 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/contrib/propagators/aws v1.37.0/go.mod h1:Cy8Hk2E2iSGEbsLnPUdeigrexaAOAGIAmBFK919EQs0= go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0/go.mod h1:hOfBCz8kv/wuq73Mx2H2QnWokh/kHZxkh6SNF2bdKtw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= @@ -1265,10 +1648,13 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -1278,6 +1664,7 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -1290,6 +1677,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= gocloud.dev v0.44.0 h1:iVyMAqFl2r6xUy7M4mfqwlN+21UpJoEtgHEcfiLMUXs= gocloud.dev v0.44.0/go.mod h1:ZmjROXGdC/eKZLF1N+RujDlFRx3D+4Av2thREKDMVxY= +gocloud.dev/pubsub/kafkapubsub v0.44.0/go.mod h1:/gcNz6OG4HgcY+w2LXwwY4qaRMgtq+SXoPSQU2jOlcw= +gocloud.dev/pubsub/natspubsub v0.44.0/go.mod h1:PvVAGIhL14PWGwWIXX/zAK42ixr2/PKP4Q4yMiAUraQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -1516,6 +1905,7 @@ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1614,6 +2004,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1626,6 +2018,8 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg= +gonum.org/v1/tools v0.0.0-20200318103217-c168b003ce8c/go.mod h1:fy6Otjqbk477ELp8IXTpw1cObQtLbRCBVonY+bTTfcM= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1651,6 +2045,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genai v1.34.0 h1:lPRJRO+HqRX1SwFo1Xb/22nZ5MBEPUbXDl61OoDxlbY= google.golang.org/genai v1.34.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -1686,6 +2081,7 @@ google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed h1:qZW022+WR7NN5TK google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed/go.mod h1:SpjiK7gGN2j/djoQMxLl3QOe/J/XxNzC5M+YLecVVWU= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20260120174246-409b4a993575/go.mod h1:Tej9lWiwVvQJP+b43pjJIsr/3mZycXWCIyoiXmbFf40= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1703,6 +2099,7 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1735,6 +2132,9 @@ gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaD gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -1786,18 +2186,27 @@ k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpAp k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/code-generator v0.35.2/go.mod h1:id4XLCm0yAQq5nlvyfAKibMOKnMjzlesAwGw6kM3Adc= +k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kms v0.35.2/go.mod h1:VT+4ekZAdrZDMgShK37vvlyHUVhwI9t/9tvh0AyCWmQ= +k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM= k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf h1:rRz0YsF7VXj9fXRF6yQgFI7DzST+hsI3TeFSGupntu0= layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= @@ -1824,9 +2233,13 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/gateway-api v1.5.0 h1:duoo14Ky/fJXpjpmyMISE2RTBGnfCg8zICfTYLTnBJA= @@ -1835,7 +2248,9 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/tests/oidc_e2e/oidc-signing-key.pem b/tests/oidc_e2e/oidc-signing-key.pem deleted file mode 100644 index 95608dd8b..000000000 --- a/tests/oidc_e2e/oidc-signing-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAw/iXq6C+/jPwgtjaIOM2MJ50LTlM/yJgPQFn7na4MsVZ0Erj -h3AottVS7A/69BZWBvxpm6C2PGnyCv9er6DQ7t5/wB2PQ5zw9TvlNlgefPHrmOlM -oZHV7sLmA1vnRNLqBIs9LIxAJeXVR3lLG6yRlLeXuYgqgnZfQpNahcdcwWB+hAJZ -83d1lcFSUwCEa1rR7CumfrJHL6bGAk2t7aMIqIPu2PHn5hU2msXgTXwCHavFXU8U -vMTYCTHkA+eG6WStkw0YpXR7m7Q2GC9PWiMQbZJAs3QoGXsT6MpPbps45YJrpHrB -zZPrjOOb1laPjQKY6dtHnd91Fm1rDRi9K97WmwIDAQABAoIBAAT5o65/9zlW5W08 -+iF80ALHkwviErolC986N091HiBtccx5VwtrzCADWTnHy63OTCk2cUWGyZM0RI8F -t3t39rXwJu6GqzA8cY8x3q2hhEY7XtxtiwkZcOsncYipfzz4Q2hE7Kzb2dxC2HmA -pjWx2aq2R0wDg/WQWCmSU7Yy8hmxDdktoP7WZWdzpXWhLLasuUn9EatnHCk38KIW -X+l6dwzhBwW23fc56m/x2LrDMZrYGaXjGMdgIZ1meUADZ1D5vSArqcniS4QWnUco -S8iDOvJ8YEArtTAkUrLAKVOSJhQL0UBemd5JcMN7LtxcOU38gp4GpRs05gEz5ggP -J3yGWjkCgYEA7xFLNV9Mr+j61uWNWW0zdBNtnFBnhJ/fvnohbVl/bLRx3vl5c374 -/G/GbAvG/I1l719utcs2WE3M/3fD9zkUlbeuXKYRoMO5/6d22nNI/v4hcS0bi3ll -eEq0dlB+fi31ac8JmetT8P0tFU5d7WILQdBp0MBP7nbNvaFiBgnJH10CgYEA0dni -mSrNRxFVX9/EHAeQhfUPVTmWvnL1XZGec2Bp0C8E/6ogle5QYKfUqxe2wALLIUuG -aVVtui4p3M9emDdOI5CIMRjCzgtwwVKozWdD4xLLkd0xmRwNK4Aac8MTB0ADl0zF -d8G8qbPBnNmpC3DBw5Vy1Ck7BYvaBIO/5OFdBlcCgYEApC19N88VrCzUruYAV2ye -DFYXTWUOYk8k6fuXny/6SV32YYl0NbP6K+pbGvJPmjtEyMoCDsjarnPnl33ZT5uc -nmEBVlEaBAzGXGLWRZkshSljMAUpSHR7EcxD+Ii5BdBsHFj5oAGzqOlFn78s1Awj -7PPC54BapEpkapk85yarP2UCgYEApC/hhw1iBxScw6KEAZo5jVWrZXblZqTzLP5e -Bs+MeoIPWyl0zVnkMXuefS7UoW6OF8LLq2ZXr6+muUjWLio3TMQsip1g7W0uD1Pp -FqyRBP0ToVB7GdxEwaA9Eg2yuZ1wMdSzO3utbOljtqDmDjluoOQxL6YCs9g0AAxE -+4MIEYkCgYAXkbBV+vU5DgwCS9ENVZX4zM31IzfuSf/iAFuA1Oav05kXQ8XXE3S/ -D+iiEL7+sodp3bdg/yDfVTpy4YpRupx09H06ZDAkc+YuZY0Zn7cKIDTxX2AqFvui -NRfFSnuA0HqAjs85LHKZzXdEvS0wrvLVabVKcD9m3RDmW4I9qQIWuQ== ------END RSA PRIVATE KEY----- From f6cdb9a967d8b31cda37152825fbe30a5fa62111 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Wed, 15 Apr 2026 21:53:28 +0300 Subject: [PATCH 44/48] chore(deps): bump @flanksource/facet to 0.1.38 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream facet 0.1.33-0.1.38 add the Document/Page/Header/Footer structural exports required by report/Application.tsx, report/CatalogReport.tsx, and report/ViewReport.tsx. The previously pinned 0.1.32 did not export Document, causing vite SSR builds in facet-html/facet-pdf tests and the Playbook facet-pdf-report e2e to fail with "Document is not exported". Also regenerates package-lock.json — the previous lockfile was a thin file: reference so npm install never pulled facet's runtime deps into the tree. --- report/package-lock.json | 1058 +++++++++++++++++++++++++++++++++++--- report/package.json | 7 +- 2 files changed, 981 insertions(+), 84 deletions(-) diff --git a/report/package-lock.json b/report/package-lock.json index 610a1cf55..4f83249a7 100644 --- a/report/package-lock.json +++ b/report/package-lock.json @@ -8,7 +8,7 @@ "name": "application-report", "version": "1.0.0", "dependencies": { - "@flanksource/facet": "^0.1.32", + "@flanksource/facet": "^0.1.38", "@flanksource/icons": "^1.0.53", "@iconify-json/carbon": "^1.2.0", "@iconify-json/fluent": "^1.2.0", @@ -33,66 +33,6 @@ "../../../facet": { "extraneous": true }, - "../../facet": { - "name": "@flanksource/facet", - "version": "0.1.32", - "dependencies": { - "@flanksource/icons": "^1.0.53", - "@iconify/react": "^5.1.0", - "@xyflow/react": "^12.0.0", - "clsx": "^2.1.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.2.0", - "dagre": "^0.8.5", - "dayjs": "^1.11.13", - "pdf-lib": "^1.17.1", - "react-icons": "^5.4.0", - "shiki": "^1.0.0" - }, - "devDependencies": { - "@mdx-js/rollup": "^3.0.0", - "@storybook/addon-docs": "^8.6.14", - "@storybook/addon-essentials": "^8.6.14", - "@storybook/react": "^8.6.14", - "@storybook/react-vite": "^8.6.14", - "@tailwindcss/postcss": "^4.1.17", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@types/d3-scale": "^4.0.9", - "@types/d3-shape": "^3.1.8", - "@types/dagre": "^0.7.52", - "@types/node": "^20.0.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.2.0", - "autoprefixer": "^10.4.16", - "baseline-browser-mapping": "^2.10.0", - "chalk": "^5.6.2", - "chromatic": "^16.0.0", - "juice": "^11.0.3", - "ora": "^8.2.0", - "puppeteer": "^24.26.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "remark-gfm": "^4.0.0", - "storybook": "^8.6.14", - "tailwindcss": "^3.4.3", - "tsx": "^4.20.6", - "typescript": "^5.3.0", - "vite": "^5.0.0", - "vite-plugin-dts": "^4.5.0", - "vitest": "^4.1.1" - }, - "engines": { - "node": ">=20.19" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -536,8 +476,29 @@ } }, "node_modules/@flanksource/facet": { - "resolved": "../../facet", - "link": true + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@flanksource/facet/-/facet-0.1.38.tgz", + "integrity": "sha512-mn1o+lCeG2zObz32W2MB6miA+FR8RJq9scAMmwx6qzKmW4bYy3ZnymDA3ChhpYERKD2cB+64u9pwgeVx3somiw==", + "dependencies": { + "@flanksource/icons": "^1.0.53", + "@iconify/react": "^5.1.0", + "@xyflow/react": "^12.0.0", + "clsx": "^2.1.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "dagre": "^0.8.5", + "dayjs": "^1.11.13", + "pdf-lib": "^1.17.1", + "react-icons": "^5.4.0", + "shiki": "^1.0.0" + }, + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } }, "node_modules/@flanksource/icons": { "version": "1.0.53", @@ -650,6 +611,24 @@ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "license": "MIT" }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -1000,6 +979,124 @@ "win32" ] }, + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1007,6 +1104,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -1014,6 +1120,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", @@ -1025,6 +1140,50 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@xyflow/react": { + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.76", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1054,6 +1213,293 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -1142,11 +1588,69 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/js-yaml": { "version": "4.1.1", @@ -1160,16 +1664,149 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, - "bin": { - "loose-envify": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" } }, "node_modules/picomatch": { @@ -1215,19 +1852,48 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -1235,6 +1901,31 @@ "dev": true, "license": "MIT" }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1290,6 +1981,52 @@ "fsevents": "~2.3.2" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/shiki": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", + "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/langs": "1.29.2", + "@shikijs/themes": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1307,6 +2044,22 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1349,6 +2102,111 @@ "dev": true, "license": "MIT" }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", @@ -1423,6 +2281,44 @@ "optional": true } } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/report/package.json b/report/package.json index 92f1fe2e4..c304dcc09 100644 --- a/report/package.json +++ b/report/package.json @@ -9,11 +9,12 @@ "mission-control": "npm run pdf" }, "dependencies": { - "@flanksource/facet": "^0.1.32", + "@flanksource/facet": "^0.1.38", "@flanksource/icons": "^1.0.53", - "@iconify-json/carbon": "^1.2.0", + "@iconify-json/carbon": "^1.2.0", "@iconify-json/fluent": "^1.2.0", - "@iconify-json/iconoir": "^1.2.0", "@iconify-json/lucide": "^1.2.0", + "@iconify-json/iconoir": "^1.2.0", + "@iconify-json/lucide": "^1.2.0", "@iconify-json/mdi": "^1.2.0", "@iconify-json/ph": "^1.2.0", "@iconify-json/ri": "^1.2.0", From 6333760aa764e981ffd9dc628218cddf167c5c2e Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Thu, 16 Apr 2026 07:09:44 +0300 Subject: [PATCH 45/48] refactor(report): improve report rendering with conditional sections and error handling - Add section visibility flags to conditionally render report components - Consolidate artifact changes from multiple sources in ArtifactAppendix - Extract timestamp generation to avoid multiple Date instantiations - Wrap Document component around Page in MatrixDemo for proper structure - Add early return for empty views in ViewReport - Improve error handling in change_mappings with duty API errors - Optimize date calculations in BackupActivityCalendar using string comparisons - Extract failure status check into reusable function in BackupsSection - Fix section title capitalization in ConfigChangesExamples - Refactor analysis type grouping to handle unknown types gracefully - Optimize deployment change categorization with single pass - Add link support to GitRef component - Improve stale color logic in RBACMatrixSection - Add TimestampURL option to RenderHTTP for PDF signing - Use time.RFC3339 constant for date formatting - Improve error differentiation in scraper query handling --- report/CatalogReport.tsx | 33 +++++++++++--------- report/KitchenSink.tsx | 5 +-- report/MatrixDemo.tsx | 6 ++-- report/ViewReport.tsx | 1 + report/catalog/change_mappings.go | 11 ++++--- report/components/BackupActivityCalendar.tsx | 16 ++++++---- report/components/BackupsSection.tsx | 7 +++-- report/components/ConfigChangesExamples.tsx | 2 +- report/components/ConfigInsightsSection.tsx | 11 ++++--- report/components/DeploymentChanges.tsx | 33 ++++++++++---------- report/components/GitRef.tsx | 7 ++++- report/components/RBACMatrixSection.tsx | 3 +- report/facet.go | 14 +++++++-- report/scraper/scraper.go | 12 +++++-- 14 files changed, 100 insertions(+), 61 deletions(-) diff --git a/report/CatalogReport.tsx b/report/CatalogReport.tsx index eeaf0cba9..cb0ffb24a 100644 --- a/report/CatalogReport.tsx +++ b/report/CatalogReport.tsx @@ -164,36 +164,41 @@ export default function CatalogReportPage({ data }: CatalogReportProps) { {data.groupBy !== 'config' && ( <> - - + {data.sections?.changes && } + {data.sections?.insights && } )} - {data.relationshipTree - ? - : - } + {data.sections?.relationships && ( + data.relationshipTree + ? + : + )} {data.groupBy !== 'config' && ( <> - - + {data.sections?.access && } + {data.sections?.accessLogs && } )} {data.groupBy === 'config' && (data.configGroups || []).map((group, idx) => ( - - - - + {data.sections?.changes && } + {data.sections?.insights && } + {data.sections?.access && } + {data.sections?.accessLogs && } ))} - {data.configJSON && } + {data.sections?.configJSON && data.configJSON && } - e.changes || [])} /> + e.changes ?? []), + ...(data.configGroups ?? []).flatMap((g) => g.changes ?? []), + ]} /> {data.audit && ( diff --git a/report/KitchenSink.tsx b/report/KitchenSink.tsx index 4afcd3ace..114fad824 100644 --- a/report/KitchenSink.tsx +++ b/report/KitchenSink.tsx @@ -19,9 +19,10 @@ interface KitchenSinkProps { } export default function KitchenSink({ data }: KitchenSinkProps) { + const generatedAt = new Date().toISOString(); const header = ; - const footer = ; + const footer = ; const pageProps = { pageSize: 'a4' as const, margins: { top: 5, bottom: 5, left: 5, right: 5 }, @@ -42,7 +43,7 @@ export default function KitchenSink({ data }: KitchenSinkProps) { { label: 'insights', value: data.analyses.length }, { label: 'relationships', value: data.relationships.length }, ]} - generatedAt={new Date().toISOString()} + generatedAt={generatedAt} >

PDF-compatible components for rendering config items, changes, insights, and relationships. diff --git a/report/MatrixDemo.tsx b/report/MatrixDemo.tsx index 274ee5b43..d4f87b85a 100644 --- a/report/MatrixDemo.tsx +++ b/report/MatrixDemo.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Page } from '@flanksource/facet'; +import { Document, Page } from '@flanksource/facet'; import { MatrixTable, Dot } from '@flanksource/facet'; import { AccessIndicator, @@ -35,7 +35,8 @@ export default function MatrixDemo() { const roles = ['Read', 'Write', 'Execute', 'Admin', 'Delete', 'Audit']; return ( - + +

RBAC Matrix - Visual System Demo

@@ -212,5 +213,6 @@ export default function MatrixDemo() {
+ ); } diff --git a/report/ViewReport.tsx b/report/ViewReport.tsx index 47ed3ad6b..66612527b 100644 --- a/report/ViewReport.tsx +++ b/report/ViewReport.tsx @@ -32,6 +32,7 @@ interface ViewReportProps { export default function ViewReportPage({ data }: ViewReportProps) { const viewsList = isMultiView(data) ? data.views : [data]; + if (!viewsList.length) return null; const firstView = viewsList[0]; return ( diff --git a/report/catalog/change_mappings.go b/report/catalog/change_mappings.go index 3c46ddf89..425fa0376 100644 --- a/report/catalog/change_mappings.go +++ b/report/catalog/change_mappings.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" + dutyAPI "github.com/flanksource/duty/api" dutyContext "github.com/flanksource/duty/context" reportAPI "github.com/flanksource/incident-commander/api" "github.com/google/cel-go/cel" @@ -52,28 +53,28 @@ func newChangeMapper(ctx dutyContext.Context, mappings []reportAPI.CatalogReport env, err := cel.NewEnv(envOptions...) if err != nil { - return nil, err + return nil, ctx.Oops().Wrap(err) } compiled := make([]compiledCategoryMapping, 0, len(mappings)) for i, mapping := range mappings { if mapping.Filter == "" { - return nil, fmt.Errorf("categoryMappings[%d] filter is required", i) + return nil, dutyAPI.Errorf(dutyAPI.EINVALID, "categoryMappings[%d] filter is required", i) } if mapping.Category == "" && mapping.Transform == "" { - return nil, fmt.Errorf("categoryMappings[%d] must define category or transform", i) + return nil, dutyAPI.Errorf(dutyAPI.EINVALID, "categoryMappings[%d] must define category or transform", i) } filter, err := compileChangeMappingProgram(env, mapping.Filter) if err != nil { - return nil, fmt.Errorf("failed to compile categoryMappings[%d] filter: %w", i, err) + return nil, ctx.Oops().Wrapf(err, "failed to compile categoryMappings[%d] filter", i) } var transform cel.Program if mapping.Transform != "" { transform, err = compileChangeMappingProgram(env, mapping.Transform) if err != nil { - return nil, fmt.Errorf("failed to compile categoryMappings[%d] transform: %w", i, err) + return nil, ctx.Oops().Wrapf(err, "failed to compile categoryMappings[%d] transform", i) } } diff --git a/report/components/BackupActivityCalendar.tsx b/report/components/BackupActivityCalendar.tsx index 2a512301e..fafeebfe3 100644 --- a/report/components/BackupActivityCalendar.tsx +++ b/report/components/BackupActivityCalendar.tsx @@ -69,12 +69,14 @@ export default function BackupActivityCalendar({ entries }: Props) { } const aggregated = aggregateEntries(entries); - const referenceDate = new Date(aggregated.reduce((latest, entry) => ( - new Date(entry.date).getTime() > new Date(latest.date).getTime() ? entry : latest - )).date); + const referenceKey = aggregated.reduce((latest, entry) => { + const key = entry.date.slice(0, 10); + return key > latest ? key : latest; + }, aggregated[0].date.slice(0, 10)); - const year = referenceDate.getFullYear(); - const month = referenceDate.getMonth(); + const [yearStr, monthStr] = referenceKey.split('-'); + const year = Number(yearStr); + const month = Number(monthStr) - 1; const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDow = new Date(year, month, 1).getDay(); @@ -83,7 +85,9 @@ export default function BackupActivityCalendar({ entries }: Props) { dateMap[entry.date.slice(0, 10)] = entry; } - const monthLabel = referenceDate.toLocaleString('default', { month: 'long', year: 'numeric' }); + const monthLabel = new Date(Date.UTC(year, month, 1)).toLocaleString('en-US', { + month: 'long', year: 'numeric', timeZone: 'UTC', + }); const cells: (number | null)[] = [ ...Array(firstDow).fill(null), ...Array.from({ length: daysInMonth }, (_, index) => index + 1), diff --git a/report/components/BackupsSection.tsx b/report/components/BackupsSection.tsx index d18baeb3f..071f30c0c 100644 --- a/report/components/BackupsSection.tsx +++ b/report/components/BackupsSection.tsx @@ -30,15 +30,16 @@ const BACKUP_TAG_MAPPING = (key: string, value: unknown): string => { }; export default function BackupsSection({ backups, restores }: Props) { + const isFailed = (status: string) => String(status).toLowerCase().includes('fail'); const successCount = backups.filter((b) => b.status === 'success').length; - const failedCount = backups.filter((b) => b.status !== 'success').length; + const failedCount = backups.filter((b) => isFailed(b.status)).length; const calendarEntries = backups.map((backup) => ({ date: backup.date, - status: (backup.status === 'success' ? 'success' : backup.status === 'failed' ? 'failed' : 'warning') as BackupCalendarStatus, + status: (backup.status === 'success' ? 'success' : isFailed(backup.status) ? 'failed' : 'warning') as BackupCalendarStatus, label: backup.size || undefined, })); - const failedRows = backups.filter((b) => b.status !== 'success'); + const failedRows = backups.filter((b) => isFailed(b.status)); return (
diff --git a/report/components/ConfigChangesExamples.tsx b/report/components/ConfigChangesExamples.tsx index 8c85b8c98..706276e8c 100644 --- a/report/components/ConfigChangesExamples.tsx +++ b/report/components/ConfigChangesExamples.tsx @@ -51,7 +51,7 @@ export default function ConfigChangesExamples({ changes }: Props) { return ( <> {singleLine.length > 0 && ( -
+
Compact rows optimized for one-line scanning. Change type, diff chips, config, actor, counters, and severity stay inline whenever the summary is short enough.
diff --git a/report/components/ConfigInsightsSection.tsx b/report/components/ConfigInsightsSection.tsx index fa53ff103..642b9a004 100644 --- a/report/components/ConfigInsightsSection.tsx +++ b/report/components/ConfigInsightsSection.tsx @@ -86,9 +86,12 @@ export default function ConfigInsightsSection({ analyses }: Props) { const bySeverity = Object.fromEntries( SEVERITY_ORDER.map((sev) => [sev, analyses.filter((a) => (a.severity ?? 'info') === sev).length]) ); - const byType = Object.fromEntries( - ANALYSIS_TYPES.map((t) => [t, analyses.filter((a) => a.analysisType === t)]) - ); + const byType: Record = {}; + for (const a of analyses) { + const t = a.analysisType && ANALYSIS_TYPES.includes(a.analysisType as AnalysisType) ? a.analysisType : 'other'; + (byType[t] ??= []).push(a); + } + const typeOrder = [...ANALYSIS_TYPES.filter((t) => byType[t]?.length), ...(byType['other']?.length ? ['other' as const] : [])]; return (
@@ -103,7 +106,7 @@ export default function ConfigInsightsSection({ analyses }: Props) {
))}
- {ANALYSIS_TYPES.map((type) => ( + {typeOrder.map((type) => ( ))}
diff --git a/report/components/DeploymentChanges.tsx b/report/components/DeploymentChanges.tsx index 24758adef..55827456f 100644 --- a/report/components/DeploymentChanges.tsx +++ b/report/components/DeploymentChanges.tsx @@ -47,11 +47,15 @@ export default function DeploymentChanges({ changes }: Props) { return null; } - const counts = { - scale: relevant.filter((change) => classifyDeploymentChange(change) === 'scale').length, - policy: relevant.filter((change) => classifyDeploymentChange(change) === 'policy').length, - spec: relevant.filter((change) => classifyDeploymentChange(change) === 'spec').length, - }; + const categorized = relevant.map((change) => ({ + change, + category: classifyDeploymentChange(change) ?? 'spec', + })); + + const counts = categorized.reduce( + (acc, { category }) => { acc[category] += 1; return acc; }, + { scale: 0, policy: 0, spec: 0 } as Record, + ); return ( <> @@ -102,17 +106,14 @@ export default function DeploymentChanges({ changes }: Props) {
{ - const category = classifyDeploymentChange(change) ?? 'spec'; - return { - id: change.id, - date: change.date, - subject: change.description, - subtitle: change.changeType ?? '-', - category: CATEGORY_LABELS[category], - actor: getChangeActor(change), - }; - })} + rows={categorized.map(({ change, category }) => ({ + id: change.id, + date: change.date, + subject: change.description, + subtitle: change.changeType ?? '-', + category: CATEGORY_LABELS[category], + actor: getChangeActor(change), + }))} subject="subject" subtitle="subtitle" date="date" diff --git a/report/components/GitRef.tsx b/report/components/GitRef.tsx index 76c33c284..b7e68ab68 100644 --- a/report/components/GitRef.tsx +++ b/report/components/GitRef.tsx @@ -35,7 +35,7 @@ export default function GitRef({ url, branch, file, dir, link, size = 'xs' }: Gi if (!url && !file) return null; const textClass = SIZE_CLASSES[size]; - return ( + const content = ( {url && {url}{branch ? ` @ ${branch}` : ''}} @@ -43,6 +43,11 @@ export default function GitRef({ url, branch, file, dir, link, size = 'xs' }: Gi {file && {file}} ); + + if (link) { + return {content}; + } + return content; } export function GitRefFromSource({ gitops, size }: { gitops?: { git: { url: string; branch: string; file: string; dir: string; link: string } }; size?: 'xs' | 'sm' }) { diff --git a/report/components/RBACMatrixSection.tsx b/report/components/RBACMatrixSection.tsx index 8b88ab4f9..2f04fa2e3 100644 --- a/report/components/RBACMatrixSection.tsx +++ b/report/components/RBACMatrixSection.tsx @@ -41,7 +41,8 @@ function loginAgeDays(lastSignedInAt?: string | null): number | null { function staleColor(lastSignedInAt?: string | null): string | null { const days = loginAgeDays(lastSignedInAt); - if (days === null || days > 30) return STALE_COLORS.stale30d; + if (days === null) return null; + if (days > 30) return STALE_COLORS.stale30d; if (days > 7) return STALE_COLORS.stale7d; return null; } diff --git a/report/facet.go b/report/facet.go index 51ba871e6..19193c082 100644 --- a/report/facet.go +++ b/report/facet.go @@ -106,8 +106,12 @@ func RenderCLI(data any, format, entryFile string) (*RenderResult, error) { }, nil } +type RenderHTTPOptions struct { + TimestampURL string +} + // RenderHTTP renders data via a remote facet rendering service. -func RenderHTTP(ctx context.Context, baseURL, token string, data any, format, entryFile string) ([]byte, error) { +func RenderHTTP(ctx context.Context, baseURL, token string, data any, format, entryFile string, opts ...RenderHTTPOptions) ([]byte, error) { archive, err := BuildArchive() if err != nil { return nil, fmt.Errorf("build report archive: %w", err) @@ -118,10 +122,14 @@ func RenderHTTP(ctx context.Context, baseURL, token string, data any, format, en return nil, fmt.Errorf("marshal data: %w", err) } - optionsJSON, err := json.Marshal(map[string]any{ + renderOpts := map[string]any{ "format": format, "entryFile": entryFile, - }) + } + if len(opts) > 0 && opts[0].TimestampURL != "" { + renderOpts["timestampUrl"] = opts[0].TimestampURL + } + optionsJSON, err := json.Marshal(renderOpts) if err != nil { return nil, fmt.Errorf("marshal options: %w", err) } diff --git a/report/scraper/scraper.go b/report/scraper/scraper.go index 0965c0f38..5471f19c7 100644 --- a/report/scraper/scraper.go +++ b/report/scraper/scraper.go @@ -3,13 +3,16 @@ package scraper import ( "crypto/sha256" "encoding/json" + "errors" "fmt" "sort" + "time" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" "github.com/flanksource/duty/query" "github.com/google/uuid" + "gorm.io/gorm" "github.com/flanksource/incident-commander/api" ) @@ -32,7 +35,10 @@ var knownBackendKeys = map[string]bool{ func BuildScraperInfo(ctx context.Context, scraperID uuid.UUID) (*api.ScraperInfo, error) { var scraper models.ConfigScraper if err := ctx.DB().Where("id = ?", scraperID).First(&scraper).Error; err != nil { - return nil, ctx.Oops().Wrapf(err, "scraper %s not found", scraperID) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ctx.Oops().Wrapf(err, "scraper %s not found", scraperID) + } + return nil, ctx.Oops().Wrapf(err, "failed to query scraper %s", scraperID) } types := parseSpecTypes(scraper.Spec) @@ -45,7 +51,7 @@ func BuildScraperInfo(ctx context.Context, scraperID uuid.UUID) (*api.ScraperInf Name: scraper.Name, Namespace: scraper.Namespace, Source: scraper.Source, - CreatedAt: scraper.CreatedAt.Format("2006-01-02T15:04:05Z"), + CreatedAt: scraper.CreatedAt.Format(time.RFC3339), SpecHash: specSHA256(scraper.Spec), Types: types, } @@ -55,7 +61,7 @@ func BuildScraperInfo(ctx context.Context, scraperID uuid.UUID) (*api.ScraperInf } if scraper.UpdatedAt != nil { - info.UpdatedAt = scraper.UpdatedAt.Format("2006-01-02T15:04:05Z") + info.UpdatedAt = scraper.UpdatedAt.Format(time.RFC3339) } if scraper.CreatedBy != nil { From 608dffaa7c1f3996e7ca37dbab0d164ad754886b Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Thu, 16 Apr 2026 07:10:46 +0300 Subject: [PATCH 46/48] refactor(auth): Replace custom StringList with pq.StringArray Remove custom StringList type and its Value/Scan implementations in favor of the standard pq.StringArray from lib/pq. This simplifies the codebase by using a well-tested PostgreSQL array type instead of maintaining custom serialization logic. Also improve error handling in RandomBase64 by checking rand.Read errors instead of silently ignoring them, and add token expiration check in extractBearerTokens to skip expired tokens. Remove unused sessionStorage serialization from saveConnection. --- auth/oidc/models.go | 82 +++++++++++------------------------ auth/oidc/storage.go | 5 ++- auth/oidcclient/oidcclient.go | 4 +- cmd/connection_browser.go | 10 ++--- 4 files changed, 35 insertions(+), 66 deletions(-) diff --git a/auth/oidc/models.go b/auth/oidc/models.go index 0aefb5cc8..626600a2b 100644 --- a/auth/oidc/models.go +++ b/auth/oidc/models.go @@ -1,62 +1,32 @@ package oidc import ( - "database/sql/driver" - "fmt" - "strings" "time" + "github.com/lib/pq" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" ) const ClientID = "mc-cli" -// StringList is a PostgreSQL text[] compatible type. -type StringList []string - -func (s StringList) Value() (driver.Value, error) { - if len(s) == 0 { - return "{}", nil - } - return "{" + strings.Join(s, ",") + "}", nil -} - -func (s *StringList) Scan(src any) error { - if src == nil { - *s = nil - return nil - } - str, ok := src.(string) - if !ok { - return fmt.Errorf("unsupported type: %T", src) - } - str = strings.Trim(str, "{}") - if str == "" { - *s = nil - return nil - } - *s = strings.Split(str, ",") - return nil -} - // AuthRequest implements op.AuthRequest backed by the oidc_auth_requests table. type AuthRequest struct { - ID string `gorm:"primaryKey;column:id"` - ClientID string `gorm:"column:client_id;not null"` - RedirectURI string `gorm:"column:redirect_uri;not null"` - Scopes StringList `gorm:"column:scopes;type:text[]"` - State string `gorm:"column:state"` - Nonce string `gorm:"column:nonce"` - ResponseType string `gorm:"column:response_type;not null"` - CodeChallenge string `gorm:"column:code_challenge"` - CodeChallengeMethod string `gorm:"column:code_challenge_method"` - Subject string `gorm:"column:subject"` - AuthTime *time.Time `gorm:"column:auth_time"` - Code *string `gorm:"column:code"` - IsDone bool `gorm:"column:done;default:false"` - CreatedAt time.Time `gorm:"column:created_at"` - ExpiresAt time.Time `gorm:"column:expires_at"` + ID string `gorm:"primaryKey;column:id"` + ClientID string `gorm:"column:client_id;not null"` + RedirectURI string `gorm:"column:redirect_uri;not null"` + Scopes pq.StringArray `gorm:"column:scopes;type:text[]"` + State string `gorm:"column:state"` + Nonce string `gorm:"column:nonce"` + ResponseType string `gorm:"column:response_type;not null"` + CodeChallenge string `gorm:"column:code_challenge"` + CodeChallengeMethod string `gorm:"column:code_challenge_method"` + Subject string `gorm:"column:subject"` + AuthTime *time.Time `gorm:"column:auth_time"` + Code *string `gorm:"column:code"` + IsDone bool `gorm:"column:done;default:false"` + CreatedAt time.Time `gorm:"column:created_at"` + ExpiresAt time.Time `gorm:"column:expires_at"` } func (AuthRequest) TableName() string { return "oidc_auth_requests" } @@ -94,15 +64,15 @@ func (a *AuthRequest) Done() bool { return a.IsDone } // RefreshToken is backed by the oidc_refresh_tokens table. type RefreshToken struct { - ID string `gorm:"primaryKey;column:id"` - Token string `gorm:"column:token;not null;uniqueIndex"` - ClientID string `gorm:"column:client_id;not null"` - Subject string `gorm:"column:subject;not null"` - Scopes StringList `gorm:"column:scopes;type:text[]"` - AuthTime time.Time `gorm:"column:auth_time;not null"` - RotationID string `gorm:"column:rotation_id;not null"` - CreatedAt time.Time `gorm:"column:created_at"` - ExpiresAt time.Time `gorm:"column:expires_at"` + ID string `gorm:"primaryKey;column:id"` + Token string `gorm:"column:token;not null;uniqueIndex"` + ClientID string `gorm:"column:client_id;not null"` + Subject string `gorm:"column:subject;not null"` + Scopes pq.StringArray `gorm:"column:scopes;type:text[]"` + AuthTime time.Time `gorm:"column:auth_time;not null"` + RotationID string `gorm:"column:rotation_id;not null"` + CreatedAt time.Time `gorm:"column:created_at"` + ExpiresAt time.Time `gorm:"column:expires_at"` } func (RefreshToken) TableName() string { return "oidc_refresh_tokens" } @@ -114,7 +84,7 @@ func (r *RefreshToken) GetClientID() string { return r.ClientID } func (r *RefreshToken) GetScopes() []string { return []string(r.Scopes) } func (r *RefreshToken) GetSubject() string { return r.Subject } func (r *RefreshToken) SetCurrentScopes(scopes []string) { - r.Scopes = StringList(scopes) + r.Scopes = pq.StringArray(scopes) } // PublicKey is backed by the oidc_public_keys table. diff --git a/auth/oidc/storage.go b/auth/oidc/storage.go index 476fefbf3..4ec904762 100644 --- a/auth/oidc/storage.go +++ b/auth/oidc/storage.go @@ -16,6 +16,7 @@ import ( "github.com/flanksource/duty/models" "github.com/go-jose/go-jose/v4" "github.com/google/uuid" + "github.com/lib/pq" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "gorm.io/gorm" @@ -61,7 +62,7 @@ func (s *Storage) CreateAuthRequest(_ gocontext.Context, req *oidc.AuthRequest, ID: uuid.New().String(), ClientID: req.ClientID, RedirectURI: req.RedirectURI, - Scopes: StringList(req.Scopes), + Scopes: pq.StringArray(req.Scopes), State: req.State, Nonce: req.Nonce, ResponseType: string(req.ResponseType), @@ -135,7 +136,7 @@ func (s *Storage) CreateAccessAndRefreshTokens(_ gocontext.Context, req op.Token Token: hashToken(rawRefreshToken), ClientID: clientID, Subject: req.GetSubject(), - Scopes: StringList(req.GetScopes()), + Scopes: pq.StringArray(req.GetScopes()), AuthTime: now, RotationID: rotationID, CreatedAt: now, diff --git a/auth/oidcclient/oidcclient.go b/auth/oidcclient/oidcclient.go index cb9b211eb..41bfe3b0b 100644 --- a/auth/oidcclient/oidcclient.go +++ b/auth/oidcclient/oidcclient.go @@ -54,7 +54,9 @@ func GeneratePKCE() (verifier, challenge string, err error) { func RandomBase64(n int) string { b := make([]byte, n) - _, _ = rand.Read(b) + if _, err := rand.Read(b); err != nil { + panic("crypto/rand failed: " + err.Error()) + } return base64.RawURLEncoding.EncodeToString(b) } diff --git a/cmd/connection_browser.go b/cmd/connection_browser.go index 6591db2a0..b484eb5d0 100644 --- a/cmd/connection_browser.go +++ b/cmd/connection_browser.go @@ -460,6 +460,9 @@ func extractBearerTokens(session map[string]string) map[string]string { if jwt == nil || jwt.Audience == "" { continue } + if !jwt.ExpiresAt.IsZero() && time.Until(jwt.ExpiresAt) <= 0 { + continue + } if jwt.ScopeCount() > scopeCounts[jwt.Audience] { tokens[jwt.Audience] = secret scopeCounts[jwt.Audience] = jwt.ScopeCount() @@ -527,13 +530,6 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe } props["storageState"] = string(storageJSON) - if len(data.SessionStorage) > 0 { - sessionJSON, err := json.Marshal(data.SessionStorage) - if err == nil { - props["sessionStorage"] = string(sessionJSON) - } - } - // Also store cookies as headers for HTTP connection compatibility if len(data.Cookies) > 0 { parts := make([]string, len(data.Cookies)) From 5771d74e6e3b45e56f2ce6c0799120b55597e036 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Thu, 16 Apr 2026 07:12:11 +0300 Subject: [PATCH 47/48] refactor(cmd,db): improve error handling and remove duplicate Makefile rules - Remove duplicate Tailwind configuration and build rules in Makefile - Add $(LOCALBIN) dependency to ci-test target - Remove manual database transaction management from catalog query commands - Improve error handling in catalog_report and catalog_tree commands by returning errors instead of fataling - Use duration.ParseDuration for consistent duration parsing with proper error handling in db/applications.go - Display all external IDs in catalog details instead of just the first one - Add validation for tree direction parameter - Enhance error messages with context wrapping in RBAC and views rendering - Pass timestampURL to RenderHTTP in views rendering --- Makefile | 8 +------- cmd/catalog.go | 5 ----- cmd/catalog_get.go | 2 +- cmd/catalog_report.go | 17 +++++++++++------ cmd/catalog_tree.go | 8 ++++++++ db/applications.go | 19 +++++++++++++------ rbac_report/render_facet.go | 2 +- views/render_facet.go | 6 ++++-- 8 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 622e905b4..92e60e42e 100644 --- a/Makefile +++ b/Makefile @@ -39,12 +39,6 @@ GOLANGCI_LINT_VERSION ?= 2.11.4 TAILWIND_VERSION ?= 3.4.17 TAILWIND_JS = auth/oidc/static/tailwind.min.js -$(TAILWIND_JS): - curl -sL "https://cdn.tailwindcss.com/$(TAILWIND_VERSION)" -o $(TAILWIND_JS) - -TAILWIND_VERSION ?= 3.4.17 -TAILWIND_JS = auth/oidc/static/tailwind.min.js - $(TAILWIND_JS): curl -sL "https://cdn.tailwindcss.com/$(TAILWIND_VERSION)" -o $(TAILWIND_JS) @@ -64,7 +58,7 @@ test: --succinct --label-filter='!ignore_local' .PHONY: ci-test -ci-test: $(TAILWIND_JS) +ci-test: $(TAILWIND_JS) $(LOCALBIN) go build -o ./.bin/$(NAME) main.go ginkgo -r --skip-package=tests/e2e --keep-going --junit-report junit-report.xml --github-output --output-dir test-reports --succinct diff --git a/cmd/catalog.go b/cmd/catalog.go index a7baaf5e7..58c83c3c5 100644 --- a/cmd/catalog.go +++ b/cmd/catalog.go @@ -50,23 +50,18 @@ var Query = &cobra.Command{ start := time.Now() var response *query.SearchResourcesResponse - ctx.DB().Begin() response, err = query.SearchResources(ctx, req) if err != nil { logger.Fatalf(err.Error()) os.Exit(1) } - ctx.DB().Commit() for time.Since(start) < catalogWaitFor { if len(response.Configs) > 0 || len(response.Components) > 0 || len(response.Checks) > 0 { break } - ctx.DB().Begin() response, err = query.SearchResources(ctx, req) - ctx.DB().Commit() - if err != nil { logger.Fatalf(err.Error()) os.Exit(1) diff --git a/cmd/catalog_get.go b/cmd/catalog_get.go index 4bdcebd1d..814821004 100644 --- a/cmd/catalog_get.go +++ b/cmd/catalog_get.go @@ -283,7 +283,7 @@ func buildDetailsSection(r CatalogGetResult) api.DescriptionList { items = append(items, api.KeyValuePair{Key: "Parent", Value: c.ParentID.String()}) } if len(c.ExternalID) > 0 { - items = append(items, api.KeyValuePair{Key: "External ID", Value: c.ExternalID[0]}) + items = append(items, api.KeyValuePair{Key: "External ID", Value: strings.Join(c.ExternalID, ", ")}) } if c.CostTotal30d > 0 { diff --git a/cmd/catalog_report.go b/cmd/catalog_report.go index 4a66ddb12..02bd10e28 100644 --- a/cmd/catalog_report.go +++ b/cmd/catalog_report.go @@ -72,7 +72,10 @@ Examples: shutdown.AddHookWithPriority("database", shutdown.PriorityCritical, stop) shutdown.WaitForSignal() - opts := buildCatalogReportOptions() + opts, err := buildCatalogReportOptions() + if err != nil { + return err + } queryArgs := args if opts.Settings != nil { @@ -124,7 +127,7 @@ Examples: }, } -func buildCatalogReportOptions() catalog.Options { +func buildCatalogReportOptions() (catalog.Options, error) { opts := catalog.Options{ Title: catalogReportTitle, Recursive: catalogReportRecursive, @@ -145,19 +148,21 @@ func buildCatalogReportOptions() catalog.Options { } if catalogReportSince != "" { - if d, err := duration.ParseDuration(catalogReportSince); err == nil { - opts.Since = time.Duration(d) + d, err := duration.ParseDuration(catalogReportSince) + if err != nil { + return catalog.Options{}, fmt.Errorf("invalid --since: %w", err) } + opts.Since = time.Duration(d) } settings, settingsSource, err := catalog.ResolveSettings(catalogReportSettings) if err != nil { - logger.Fatalf("failed to load settings: %v", err) + return catalog.Options{}, fmt.Errorf("failed to load settings: %w", err) } opts.Settings = settings opts.SettingsPath = settingsSource - return opts + return opts, nil } func init() { diff --git a/cmd/catalog_tree.go b/cmd/catalog_tree.go index 7cf4be978..f1ebfaa30 100644 --- a/cmd/catalog_tree.go +++ b/cmd/catalog_tree.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/flanksource/clicky" "github.com/flanksource/clicky/api" "github.com/flanksource/commons/logger" @@ -128,6 +130,12 @@ func runCatalogTree(ctx context.Context, args []string) (*CatalogTreeResult, err return nil, err } + switch treeDirection { + case "all", "incoming", "outgoing": + default: + return nil, fmt.Errorf("invalid --direction %q: must be all, incoming, or outgoing", treeDirection) + } + relType := query.Hard if treeSoft { relType = query.Both diff --git a/db/applications.go b/db/applications.go index cb475bc05..ab18a9d41 100644 --- a/db/applications.go +++ b/db/applications.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/flanksource/commons/duration" "github.com/flanksource/duty" "github.com/flanksource/duty/context" "github.com/flanksource/duty/models" @@ -589,9 +590,11 @@ func GetAccessForUIRef(ctx context.Context, filters *api.AccessUIFilters) ([]api var staleCutoff time.Time if filters.Stale != "" { - if d, err := time.ParseDuration(filters.Stale); err == nil { - staleCutoff = time.Now().Add(-d) + d, err := duration.ParseDuration(filters.Stale) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "invalid stale filter %q", filters.Stale) } + staleCutoff = time.Now().Add(-time.Duration(d)) } items := make([]api.AccessItem, 0, len(rows)) @@ -661,15 +664,19 @@ func GetAccessLogsForUIRef(ctx context.Context, filters *api.AccessLogsUIFilters } if filters.From != "" { - if d, err := time.ParseDuration(filters.From); err == nil { - q = q.Where("config_access_logs.created_at >= ?", time.Now().Add(-d)) + d, err := duration.ParseDuration(filters.From) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "invalid from filter %q", filters.From) } + q = q.Where("config_access_logs.created_at >= ?", time.Now().Add(-time.Duration(d))) } if filters.To != "" { - if d, err := time.ParseDuration(filters.To); err == nil { - q = q.Where("config_access_logs.created_at <= ?", time.Now().Add(-d)) + d, err := duration.ParseDuration(filters.To) + if err != nil { + return nil, ctx.Oops().Wrapf(err, "invalid to filter %q", filters.To) } + q = q.Where("config_access_logs.created_at <= ?", time.Now().Add(-time.Duration(d))) } switch filters.MFA { diff --git a/rbac_report/render_facet.go b/rbac_report/render_facet.go index 3b6b71a6b..f1f01c4d9 100644 --- a/rbac_report/render_facet.go +++ b/rbac_report/render_facet.go @@ -33,7 +33,7 @@ func renderWithFacet(ctx context.Context, r *api.RBACReport, format string, view result, err := report.RenderCLI(initSlices(r), format, entryFile) if err != nil { - return nil, err + return nil, ctx.Oops().Wrapf(err, "failed to render RBAC %s report", format) } ctx.Logger.V(3).Infof("Facet rendered %dKB of %s", len(result.Data)/1024, format) diff --git a/views/render_facet.go b/views/render_facet.go index f7e3d0b85..c3b437a8c 100644 --- a/views/render_facet.go +++ b/views/render_facet.go @@ -106,13 +106,15 @@ func renderFacetWithData(ctx context.Context, data any, format string, opts *v1. return nil, fmt.Errorf("data must not be nil") } - baseURL, token, _, err := resolveFacetConnection(ctx, opts) + baseURL, token, timestampURL, err := resolveFacetConnection(ctx, opts) if err != nil { return nil, err } if baseURL != "" { - return report.RenderHTTP(ctx, baseURL, token, data, format, viewEntryFile) + return report.RenderHTTP(ctx, baseURL, token, data, format, viewEntryFile, report.RenderHTTPOptions{ + TimestampURL: timestampURL, + }) } result, err := report.RenderCLI(data, format, viewEntryFile) From 391db7d216efe659f9187f7753f1b1613be9e297 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Thu, 16 Apr 2026 09:02:56 +0300 Subject: [PATCH 48/48] fix(cmd,db,report): improve error handling and fix database queries - Handle browser open errors gracefully with fallback message instead of silently failing - Change JOIN to LEFT JOIN in access logs query to include records with missing config or user references - Add scope filtering to bearer token selection for more precise token matching - Improve context add command to preserve existing values when flags are not explicitly changed - Add HTTP status code validation before parsing response body - Extract RBAC row conversion to separate function and apply limit at query level - Fix change categorization logic to properly handle uncategorized changes with fallback detection - Simplify token output formatting and add Windows support for browser opening These changes improve robustness of authentication, data retrieval, and reporting functionality. --- cmd/auth_login.go | 19 ++++----- cmd/connection_browser.go | 17 +++++--- cmd/context.go | 25 +++++++++--- db/applications.go | 4 +- report/catalog/report.go | 47 +++++++++++++++++++---- report/components/change-section-utils.ts | 16 +++++--- 6 files changed, 92 insertions(+), 36 deletions(-) diff --git a/cmd/auth_login.go b/cmd/auth_login.go index ad4959679..401079594 100644 --- a/cmd/auth_login.go +++ b/cmd/auth_login.go @@ -105,7 +105,9 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { ) fmt.Fprintf(cmd.OutOrStdout(), "Opening browser for login...\n%s\n\n", authURL) - openBrowser(authURL) + if err := openBrowser(authURL); err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "Failed to open browser: %v\nOpen the URL manually.\n\n", err) + } var code string select { @@ -131,9 +133,8 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { } fmt.Fprintf(cmd.OutOrStdout(), "\nLogin successful!\n\n") - fmt.Fprintf(cmd.OutOrStdout(), "Tokens saved to: %s\n\n", tokenPath) - fmt.Fprintf(cmd.OutOrStdout(), "Access token (expires %s):\n%s\n\n", tokens.ExpiresAt.Format("15:04:05"), tokens.AccessToken) - fmt.Fprintf(cmd.OutOrStdout(), "Refresh token:\n%s\n\n", tokens.RefreshToken) + fmt.Fprintf(cmd.OutOrStdout(), "Tokens saved to: %s\n", tokenPath) + fmt.Fprintf(cmd.OutOrStdout(), "Access token expires: %s\n", tokens.ExpiresAt.Format(time.RFC3339)) return nil } @@ -158,13 +159,13 @@ func storeTokens(serverURL string, tokens *oidcclient.Tokens) (string, error) { return path, os.WriteFile(path, data, 0600) } -func openBrowser(url string) { - var cmd string +func openBrowser(url string) error { switch runtime.GOOS { case "darwin": - cmd = "open" + return exec.Command("open", url).Start() + case "windows": + return exec.Command("cmd", "/c", "start", "", url).Start() default: - cmd = "xdg-open" + return exec.Command("xdg-open", url).Start() } - _ = exec.Command(cmd, url).Start() } diff --git a/cmd/connection_browser.go b/cmd/connection_browser.go index b484eb5d0..61dfa7a2d 100644 --- a/cmd/connection_browser.go +++ b/cmd/connection_browser.go @@ -249,7 +249,7 @@ func launchBrowserAndCapture(ctx gocontext.Context, flags browserLoginFlags) (*b } else if verbose >= 1 { fmt.Fprintln(os.Stderr, state.Pretty().ANSI()) } else { - selectedAud, _ := selectBearerToken(data.BearerTokens, flags.RequireBearerAud) + selectedAud, _ := selectBearerToken(data.BearerTokens, flags.RequireBearerAud, flags.RequireBearerScope) for _, aud := range sortedAudiences(data.BearerTokens) { if jwt := connection.DecodeJWT(data.BearerTokens[aud]); jwt != nil { t := jwt.Pretty() @@ -471,14 +471,21 @@ func extractBearerTokens(session map[string]string) map[string]string { return tokens } -func selectBearerToken(tokens map[string]string, requiredAud string) (string, error) { +func selectBearerToken(tokens map[string]string, requiredAud, requiredScope string) (string, error) { var bestAud string var bestScopes int for aud, token := range tokens { if !strings.Contains(aud, requiredAud) { continue } - if jwt := connection.DecodeJWT(token); jwt != nil && jwt.ScopeCount() > bestScopes { + jwt := connection.DecodeJWT(token) + if jwt == nil { + continue + } + if requiredScope != "" && !strings.Contains(jwt.Scopes, requiredScope) { + continue + } + if jwt.ScopeCount() > bestScopes { bestAud = aud bestScopes = jwt.ScopeCount() } @@ -563,7 +570,7 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe for aud, token := range data.BearerTokens { props["bearer_"+aud] = token } - selectedAud, err := selectBearerToken(data.BearerTokens, flags.RequireBearerAud) + selectedAud, err := selectBearerToken(data.BearerTokens, flags.RequireBearerAud, flags.RequireBearerScope) if err != nil { return err } @@ -612,7 +619,7 @@ func saveConnection(cmd *cobra.Command, flags browserLoginFlags, data *browserSe fmt.Fprintf(cmd.OutOrStdout(), " Session storage: %d keys\n", len(data.SessionStorage)) } if len(data.BearerTokens) > 0 { - selectedAud, _ := selectBearerToken(data.BearerTokens, flags.RequireBearerAud) + selectedAud, _ := selectBearerToken(data.BearerTokens, flags.RequireBearerAud, flags.RequireBearerScope) for _, aud := range sortedAudiences(data.BearerTokens) { jwt := connection.DecodeJWT(data.BearerTokens[aud]) if jwt == nil { diff --git a/cmd/context.go b/cmd/context.go index 844b75ffd..6352c67f4 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -248,13 +248,21 @@ Examples: return err } - ctx := MCContext{ - Name: contextAddName, - Server: strings.TrimRight(contextAddServer, "/"), - DB: contextAddDB, - Token: contextAddToken, + existingCtx := cfg.GetContext(contextAddName) + existing := existingCtx != nil + ctx := MCContext{Name: contextAddName} + if existingCtx != nil { + ctx = *existingCtx + } + if cmd.Flags().Changed("server") { + ctx.Server = strings.TrimRight(contextAddServer, "/") + } + if cmd.Flags().Changed("db-url") { + ctx.DB = contextAddDB + } + if cmd.Flags().Changed("token") { + ctx.Token = contextAddToken } - existing := cfg.GetContext(contextAddName) != nil cfg.SetContext(ctx) if contextAddUse || cfg.CurrentContext == "" { @@ -308,6 +316,11 @@ func ensureAPIBase(ctx *MCContext) (bool, error) { buf := make([]byte, 512) n, _ := resp.Body.Read(buf) body := strings.TrimLeft(string(buf[:n]), " \t\r\n") + switch resp.StatusCode { + case nethttp.StatusOK, nethttp.StatusUnauthorized, nethttp.StatusForbidden: + default: + return false, nil + } ct := strings.ToLower(resp.Header.Get("Content-Type")) if strings.Contains(ct, "text/html") || strings.HasPrefix(body, "<") { return false, nil diff --git a/db/applications.go b/db/applications.go index ab18a9d41..bbcb595af 100644 --- a/db/applications.go +++ b/db/applications.go @@ -638,8 +638,8 @@ func GetAccessLogsForUIRef(ctx context.Context, filters *api.AccessLogsUIFilters config_access_logs.mfa, config_access_logs.count, config_access_logs.properties`). - Joins("JOIN config_items ON config_items.id = config_access_logs.config_id"). - Joins("JOIN external_users ON external_users.id = config_access_logs.external_user_id"). + Joins("LEFT JOIN config_items ON config_items.id = config_access_logs.config_id"). + Joins("LEFT JOIN external_users ON external_users.id = config_access_logs.external_user_id"). Order("config_access_logs.created_at DESC") if filters.Search != "" { diff --git a/report/catalog/report.go b/report/catalog/report.go index e62425302..7985cd45e 100644 --- a/report/catalog/report.go +++ b/report/catalog/report.go @@ -144,6 +144,9 @@ func BuildReport(ctx context.Context, configs []models.ConfigItem, opts Options) report.Analyses = append(report.Analyses, entry.Analyses...) report.Access = append(report.Access, entry.Access...) report.AccessLogs = append(report.AccessLogs, entry.AccessLogs...) + if report.RelationshipTree == nil && entry.RelationshipTree != nil { + report.RelationshipTree = entry.RelationshipTree + } for _, id := range entryScraperIDs { scraperIDSet[id] = true @@ -283,16 +286,17 @@ func buildEntryWithMapper(ctx context.Context, config *models.ConfigItem, opts O for _, r := range entry.RBACResources { entry.AccessCount += len(r.Users) } + entry.Access = make([]api.CatalogReportAccess, 0, len(rbacRows)) + for _, row := range rbacRows { + entry.Access = append(entry.Access, rbacRowToAccess(row)) + } } if opts.Sections.AccessLogs { - logs, err := getAccessLogs(ctx, targetIDs, sinceTime) + logs, err := getAccessLogs(ctx, targetIDs, sinceTime, opts.effectiveMax(0)) if err != nil { return nil, nil, fmt.Errorf("failed to get access logs: %w", err) } - if limit := opts.effectiveMax(0); limit > 0 && len(logs) > limit { - logs = logs[:limit] - } entry.AccessLogs = lo.Map(logs, func(l accessLogRow, _ int) api.CatalogReportAccessLog { return newAccessLogEntry(l) }) @@ -435,11 +439,11 @@ func (r accessLogRow) QueryLogSummary() string { return r.ConfigType } -func getAccessLogs(ctx context.Context, configIDs []uuid.UUID, since time.Time) (results []accessLogRow, err error) { +func getAccessLogs(ctx context.Context, configIDs []uuid.UUID, since time.Time, limit int) (results []accessLogRow, err error) { timer := query.NewQueryLogger(ctx).Start("AccessLogs").Arg("configIDs", len(configIDs)) defer timer.End(&err) - if err = ctx.DB(). + q := ctx.DB(). Table("config_access_logs"). Select(`config_access_logs.config_id, config_items.name AS config_name, @@ -454,14 +458,41 @@ func getAccessLogs(ctx context.Context, configIDs []uuid.UUID, since time.Time) Joins("JOIN external_users ON external_users.id = config_access_logs.external_user_id"). Where("config_access_logs.config_id IN ?", configIDs). Where("config_access_logs.created_at >= ?", since). - Order("config_access_logs.created_at DESC"). - Scan(&results).Error; err != nil { + Order("config_access_logs.created_at DESC") + if limit > 0 { + q = q.Limit(limit) + } + if err = q.Scan(&results).Error; err != nil { return nil, err } timer.Results(results) return results, nil } +func rbacRowToAccess(r db.RBACAccessRow) api.CatalogReportAccess { + a := api.CatalogReportAccess{ + ConfigID: r.ConfigID.String(), + ConfigName: r.ConfigName, + ConfigType: r.ConfigType, + Permalink: api.ConfigPermalink(r.ConfigID.String()), + UserID: r.UserID.String(), + UserName: r.UserName, + Email: r.Email, + Role: r.Role, + UserType: r.UserType, + CreatedAt: r.CreatedAt.Format(time.RFC3339), + } + if r.LastSignedInAt != nil { + s := r.LastSignedInAt.Format(time.RFC3339) + a.LastSignedInAt = &s + } + if r.LastReviewedAt != nil { + s := r.LastReviewedAt.Format(time.RFC3339) + a.LastReviewedAt = &s + } + return a +} + func newAccessLogEntry(r accessLogRow) api.CatalogReportAccessLog { var props map[string]string if r.Properties != nil { diff --git a/report/components/change-section-utils.ts b/report/components/change-section-utils.ts index 7114b7d10..df7e5cecc 100644 --- a/report/components/change-section-utils.ts +++ b/report/components/change-section-utils.ts @@ -452,15 +452,19 @@ export function categorizeChanges( for (const change of changes) { const category = change.category ?? ''; + + if (category === 'rbac' || category.startsWith('rbac.')) { result.rbac.push(change); continue; } + if (category === 'backup' || category.startsWith('backup.')) { result.backup.push(change); continue; } + if (category === 'deployment' || category.startsWith('deployment.')) { result.deployment.push(change); continue; } + if (!category) { - result.uncategorized.push(change); - continue; + const asApp = configChangeToApplicationChange(change); + if (isRBACChange(asApp)) { result.rbac.push(change); continue; } + if (isBackupChange(asApp)) { result.backup.push(change); continue; } + if (isDeploymentChange(asApp)) { result.deployment.push(change); continue; } } - if (category === 'rbac' || category.startsWith('rbac.')) result.rbac.push(change); - else if (category === 'backup' || category.startsWith('backup.')) result.backup.push(change); - else if (category === 'deployment' || category.startsWith('deployment.')) result.deployment.push(change); - else result.uncategorized.push(change); + result.uncategorized.push(change); } return result;