diff --git a/.golangci.yaml b/.golangci.yaml index 2fedf1ead..4659ba471 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -5,6 +5,7 @@ run: linters: default: none enable: + - gocritic - bodyclose - copyloopvar - depguard @@ -76,6 +77,13 @@ linters: desc: use github.com/Masterminds/semver/v3 - pkg: github.com/hashicorp/go-version desc: use github.com/Masterminds/semver/v3 + gocritic: + disable-all: true + enabled-checks: + - ruleguard + settings: + ruleguard: + rules: "./analysis/ssrf.go" forbidigo: forbid: - pattern: ^tls\.Config$ @@ -93,6 +101,11 @@ linters: - comments - common-false-positives - legacy + rules: + - path: pkg/libhttp/ssrf + linters: + - gocritic + text: ruleguard paths: - third_party$ - builtin$ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e34f82b2..1b70dafc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,7 +147,9 @@ When adding a new route to an existing handler use the following as a template ( // @router /{id} [get] ``` -explanation about each line can be found (here)[https://github.com/swaggo/swag#api-operation] +explanation about each line can be found [here](https://github.com/swaggo/swag#api-operation) + +After changing these annotations, regenerate the TypeScript API client and types — see [Generating API types](./README.md#generating-api-types). ## Licensing diff --git a/Makefile b/Makefile index adb4a36a2..e72b77abf 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,7 @@ dev-extension: build-server build-client ## Run the extension in development mod .PHONY: docs-build docs-validate docs-clean docs-validate-clean docs-build: init-dist ## Build docs go mod download - cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./ + cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./ --overridesFile .swaggo docs-validate: docs-build ## Validate docs pnpm swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml diff --git a/README.md b/README.md index accfa31ee..883e4181e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,32 @@ You can join the Portainer Community by visiting [https://www.portainer.io/join- - Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new). - Want to help us build **_portainer_**? Follow our [contribution guidelines](https://docs.portainer.io/contribute/contribute) to build it locally and make a pull request. +## Generating API types + +The frontend consumes a TypeScript API client (SDK functions and request/response types) that is generated from the Go API's Swagger annotations. Regenerate it after any API change — a new endpoint, a changed request/response shape, or a removed endpoint: + +```bash +make generate-api +``` + +This runs the following pipeline: + +``` +Go Swagger annotations + → dist/docs/swagger.yaml (make docs-build, via swaggo/swag) + → dist/docs/openapi.yaml (swagger2openapi + validation) + → app/react/portainer/generated-api/portainer/ (hey-api/openapi-ts) +``` + +The generator is configured in [`openapi-ts.config.ts`](./openapi-ts.config.ts), which controls the output path, plugins, and tag filters (for example, `deprecated` endpoints and `edge_agent`-tagged routes are excluded). + +The generated files live in `app/react/portainer/generated-api/portainer/` and must **not** be edited by hand — your changes would be overwritten on the next run. Import the generated SDK functions and types instead of writing direct HTTP calls: + +- `@api/sdk.gen` — SDK functions +- `@api/types.gen` — request/response types + +See [Adding api docs](./CONTRIBUTING.md#adding-api-docs) for how to annotate handlers so they are picked up by the generator. + ## Security For information about reporting security vulnerabilities, please see our [Security Policy](SECURITY.md). diff --git a/analysis/ssrf.go b/analysis/ssrf.go new file mode 100644 index 000000000..3af95653b --- /dev/null +++ b/analysis/ssrf.go @@ -0,0 +1,29 @@ +//go:build ignore + +package gorules + +import "github.com/quasilyte/go-ruleguard/dsl" + +// unwrappedHTTPTransport flags http.Transport composite literals that are not +// the direct argument to ssrf.WrapTransport. +func unwrappedHTTPTransport(m dsl.Matcher) { + // Inline construction passed to a function call. + m.Match(`$f(&http.Transport{$*_})`). + Where(m["f"].Text != "ssrf.WrapTransport" && m["f"].Text != "WrapTransport" && + m["f"].Text != "ssrf.WrapTransportInternal" && m["f"].Text != "WrapTransportInternal"). + Report(`$f receives a bare *http.Transport; wrap with ssrf.WrapTransport() to enforce the SSRF protection policy`) + + // Variable assigned a bare transport (cannot be tracked to a later WrapTransport call). + m.Match(`$_ := &http.Transport{$*_}`). + Report(`bare *http.Transport variable; use ssrf.WrapTransport(&http.Transport{...}) inline instead`) +} + +// internalTransportMisuse flags calls to WrapTransportInternal outside the four proxy +// factory files where Chisel-tunnel and in-cluster K8s destinations are valid exemptions. +func internalTransportMisuse(m dsl.Matcher) { + m.Match(`ssrf.WrapTransportInternal($*_)`). + Where( + !(m.File().PkgPath.Matches(`proxy/factory`) && + m.File().Name.Matches(`^(docker|agent|local_transport|edge_transport)\.go$`))). + Report(`WrapTransportInternal bypasses SSRF validation; only valid in the kubernetes local/edge transport constructors and the docker/agent proxy factories`) +} diff --git a/analysis/tools.go b/analysis/tools.go new file mode 100644 index 000000000..55f01d8f8 --- /dev/null +++ b/analysis/tools.go @@ -0,0 +1,5 @@ +//go:build tools + +package gorules + +import _ "github.com/quasilyte/go-ruleguard/dsl" diff --git a/api/.swaggo b/api/.swaggo new file mode 100644 index 000000000..3f6089489 --- /dev/null +++ b/api/.swaggo @@ -0,0 +1 @@ +replace k8s.io/apimachinery/pkg/apis/meta/v1.Duration string diff --git a/api/cli/cli.go b/api/cli/cli.go index 5f262759d..5e1148ee5 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -56,6 +56,10 @@ func CLIFlags() *portainer.CLIFlags { TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(), CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(), CompactDB: kingpin.Flag("compact-db", "Enable database compaction on startup").Envar(portainer.CompactDBEnvVar).Default("false").Bool(), + SSRFMode: kingpin.Flag("ssrf-mode", "SSRF protection mode: off (disabled), audit (log violations but allow), enforce (block violations)").Envar("PORTAINER_SSRF_MODE").Default("off").Enum("off", "audit", "enforce"), + SSRFAllowedHosts: kingpin.Flag("ssrf-allowed-hosts", "Allowlist of hostnames (with optional wildcards), IPs, or CIDRs permitted for outbound requests. When empty and mode is enforce, all outbound connections are blocked").Envar("PORTAINER_SSRF_ALLOWED_HOSTS").Strings(), + NoSetupToken: kingpin.Flag("no-setup-token", "Disable the setup token requirement for admin initialization and restore on an uninitialized instance").Envar(portainer.NoSetupTokenEnvVar).Bool(), + SetupToken: kingpin.Flag("setup-token", "Set a custom setup token for admin initialization and restore on an uninitialized instance (overrides auto-generation)").Envar(portainer.SetupTokenEnvVar).String(), } } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 3e2432f8d..1d77814e4 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -4,6 +4,7 @@ import ( "cmp" "context" "crypto/sha256" + nethttp "net/http" "os" "path" "strings" @@ -29,6 +30,7 @@ import ( "github.com/portainer/portainer/api/http" "github.com/portainer/portainer/api/http/proxy" kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + "github.com/portainer/portainer/api/http/security/setuptoken" "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/edge/edgestacks" "github.com/portainer/portainer/api/internal/endpointutils" @@ -51,10 +53,12 @@ import ( "github.com/portainer/portainer/pkg/featureflags" "github.com/portainer/portainer/pkg/fips" "github.com/portainer/portainer/pkg/libhelm" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/portainer/portainer/pkg/libstack/compose" libswarm "github.com/portainer/portainer/pkg/libstack/swarm" "github.com/portainer/portainer/pkg/validate" + gogithttp "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/google/uuid" "github.com/rs/zerolog/log" ) @@ -225,6 +229,32 @@ func initSnapshotService( return snapshotService, nil } +func resolveSetupToken(tx dataservices.DataStoreTx, providedToken string) (string, error) { + admins, err := tx.User().UsersByRole(portainer.AdministratorRole) + if err != nil { + return "", err + } + if len(admins) > 0 { + return "", nil + } + + if providedToken != "" { + log.Info().Msg("using custom setup token; admin initialization and backup restore require this token in the X-Setup-Token header") + return providedToken, nil + } + + token, err := setuptoken.Generate() + if err != nil { + return "", err + } + + log.Info(). + Str("setup_token", token). + Msg("no administrator account configured; admin initialization and backup restore require this setup token in the X-Setup-Token header. Start with --no-setup-token to disable.") + + return token, nil +} + func initStatus(instanceID string) *portainer.Status { return &portainer.Status{ Version: portainer.APIVersion, @@ -357,6 +387,19 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow // -ce can not ever be run in FIPS mode fips.InitFIPS(false) + ssrf.Configure(ssrf.Policy{ + Mode: ssrf.Mode(*flags.SSRFMode), + AllowedHosts: *flags.SSRFAllowedHosts, + }) + + if ssrf.IsEnabled() { + if dt, ok := nethttp.DefaultTransport.(*nethttp.Transport); ok { + nethttp.DefaultTransport = ssrf.WrapTransport(dt) + } + + gogithttp.DefaultClient = gogithttp.NewClient(&nethttp.Client{Transport: nethttp.DefaultTransport}) + } + fileService := initFileService(*flags.Data) encryptionKey := loadEncryptionSecretKey(dbSecretPath(*flags.SecretKeyName)) if encryptionKey == nil { @@ -510,6 +553,17 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow } } + setupToken := "" + if adminPasswordHash == "" && !*flags.NoSetupToken { + if err := dataStore.ViewTx(func(tx dataservices.DataStoreTx) error { + var txErr error + setupToken, txErr = resolveSetupToken(tx, *flags.SetupToken) + return txErr + }); err != nil { + log.Fatal().Err(err).Msg("failed initializing setup token") + } + } + if err := reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService); err != nil { log.Fatal().Err(err).Msg("failed starting tunnel server") } @@ -601,6 +655,7 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow PlatformService: platformService, PullLimitCheckDisabled: *flags.PullLimitCheckDisabled, TrustedOrigins: trustedOrigins, + SetupToken: setupToken, } } diff --git a/api/cmd/portainer/main_test.go b/api/cmd/portainer/main_test.go index 34840a9d0..de9e4c104 100644 --- a/api/cmd/portainer/main_test.go +++ b/api/cmd/portainer/main_test.go @@ -12,6 +12,44 @@ import ( "github.com/stretchr/testify/require" ) +func Test_resolveSetupToken(t *testing.T) { + t.Parallel() + + t.Run("admin already exists — returns empty token", func(t *testing.T) { + admin := portainer.User{Role: portainer.AdministratorRole} + store := testhelpers.NewDatastore(testhelpers.WithUsers([]portainer.User{admin})) + token, err := resolveSetupToken(store, "") + require.NoError(t, err) + assert.Empty(t, token) + }) + + t.Run("no admin — generates a 64-char hex token", func(t *testing.T) { + store := testhelpers.NewDatastore(testhelpers.WithUsers([]portainer.User{})) + token, err := resolveSetupToken(store, "") + require.NoError(t, err) + assert.Len(t, token, 64) + + token2, err := resolveSetupToken(store, "") + require.NoError(t, err) + assert.NotEqual(t, token, token2) + }) + + t.Run("no admin — uses provided token", func(t *testing.T) { + store := testhelpers.NewDatastore(testhelpers.WithUsers([]portainer.User{})) + token, err := resolveSetupToken(store, "mysecrettoken") + require.NoError(t, err) + assert.Equal(t, "mysecrettoken", token) + }) + + t.Run("admin already exists — ignores provided token", func(t *testing.T) { + admin := portainer.User{Role: portainer.AdministratorRole} + store := testhelpers.NewDatastore(testhelpers.WithUsers([]portainer.User{admin})) + token, err := resolveSetupToken(store, "mysecrettoken") + require.NoError(t, err) + assert.Empty(t, token) + }) +} + const secretFileName = "secret.txt" func createPasswordFile(t *testing.T, secretPath, password string) string { diff --git a/api/crypto/nonce.go b/api/crypto/nonce.go index af1cc898b..4753374a6 100644 --- a/api/crypto/nonce.go +++ b/api/crypto/nonce.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "errors" "io" + "slices" ) type Nonce struct { @@ -45,7 +46,7 @@ func (n *Nonce) Value() []byte { func (n *Nonce) Increment() error { // Start incrementing from the least significant byte - for i := len(n.val) - 1; i >= 0; i-- { + for i := range slices.Backward(n.val) { // Increment the current byte n.val[i]++ diff --git a/api/datastore/migrate_data.go b/api/datastore/migrate_data.go index 74a033334..4a2e9de7e 100644 --- a/api/datastore/migrate_data.go +++ b/api/datastore/migrate_data.go @@ -88,6 +88,7 @@ func (store *Store) newMigratorParameters(version *models.Version, flags *portai EdgeGroupService: store.EdgeGroupService, TunnelServerService: store.TunnelServerService, PendingActionsService: store.PendingActionsService, + CustomTemplateService: store.CustomTemplateService, SourceService: store.SourceService, WorkflowService: store.WorkflowService, } diff --git a/api/datastore/migrator/migrate_2_43_0.go b/api/datastore/migrator/migrate_2_43_0.go index 54a2f38a5..6455ee61b 100644 --- a/api/datastore/migrator/migrate_2_43_0.go +++ b/api/datastore/migrator/migrate_2_43_0.go @@ -112,8 +112,8 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error { sourcesByKey := make(map[sourceDedupeKey]portainer.SourceID, len(existingSources)) for _, src := range existingSources { - if src.GitConfig != nil { - sourcesByKey[gitSourceKey(src.GitConfig)] = src.ID + if src.Git != nil { + sourcesByKey[gitSourceKey(src.Git)] = src.ID } } @@ -133,9 +133,9 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error { if !exists { src := &portainer.Source{ - Name: gittypes.RepoName(cfg.URL), - Type: portainer.SourceTypeGit, - GitConfig: cfg, + Name: gittypes.RepoName(cfg.URL), + Type: portainer.SourceTypeGit, + Git: cfg, } if err := m.sourceService.Tx(tx).Create(src); err != nil { return fmt.Errorf("failed to create source for stack %d: %w", ls.ID, err) @@ -151,14 +151,14 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error { wf := &portainer.Workflow{ Name: liveStack.Name, - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - ReferenceName: cfg.ReferenceName, - ConfigFilePath: cfg.ConfigFilePath, - ConfigHash: cfg.ConfigHash, - StackID: portainer.StackID(ls.ID), - }, - SourceIDs: []portainer.SourceID{srcID}, + Artifacts: []portainer.Artifact{{ + StackID: portainer.StackID(ls.ID), + Files: []portainer.ArtifactFile{{ + SourceID: srcID, + Path: cfg.ConfigFilePath, + Ref: cfg.ReferenceName, + Hash: cfg.ConfigHash, + }}, }}, } if err := m.workflowService.Tx(tx).Create(wf); err != nil { @@ -180,3 +180,86 @@ func (m *Migrator) migrateGitConfigToSources_2_43_0() error { return nil } + +func (m *Migrator) migrateCustomTemplateGitConfigToSources_2_43_0() error { + log.Info().Msg("migrating git-backed custom templates to Source records") + + templates, err := m.customTemplateService.ReadAll() + if err != nil { + return err + } + + existingSources, err := m.sourceService.ReadAll() + if err != nil { + return err + } + + sourcesByKey := make(map[sourceDedupeKey]portainer.SourceID, len(existingSources)) + for _, src := range existingSources { + if src.Git != nil { + sourcesByKey[gitSourceKey(src.Git)] = src.ID + } + } + + for i := range templates { + t := &templates[i] + if t.GitConfig == nil || t.Artifact != nil { + continue + } + + cfg := &gittypes.RepoConfig{ + URL: gittypes.SanitizeURL(t.GitConfig.URL), + Authentication: t.GitConfig.Authentication, + TLSSkipVerify: t.GitConfig.TLSSkipVerify, + } + + if cfg.Authentication != nil && cfg.Authentication.GitCredentialID != 0 { + log.Warn(). + Int("git_credential_id", cfg.Authentication.GitCredentialID). + Msg("custom template has a GitCredentialID reference which is not supported in CE; credential reference will be dropped during migration") + + cfg.Authentication.GitCredentialID = 0 + } + + key := gitSourceKey(cfg) + + var newSrcID portainer.SourceID + + if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error { + srcID, exists := sourcesByKey[key] + + if !exists { + src := &portainer.Source{ + Name: gittypes.RepoName(cfg.URL), + Type: portainer.SourceTypeGit, + Git: cfg, + } + if err := m.sourceService.Tx(tx).Create(src); err != nil { + return fmt.Errorf("failed to create source for custom template %d: %w", t.ID, err) + } + srcID = src.ID + newSrcID = src.ID + } + + t.Artifact = &portainer.Artifact{ + Files: []portainer.ArtifactFile{{ + SourceID: srcID, + Path: t.GitConfig.ConfigFilePath, + Ref: t.GitConfig.ReferenceName, + Hash: t.GitConfig.ConfigHash, + }}, + } + t.GitConfig = nil + + return m.customTemplateService.Tx(tx).Update(t.ID, t) + }); err != nil { + return fmt.Errorf("failed to migrate custom template %d: %w", t.ID, err) + } + + if newSrcID != 0 { + sourcesByKey[key] = newSrcID + } + } + + return nil +} diff --git a/api/datastore/migrator/migrate_2_43_0_test.go b/api/datastore/migrator/migrate_2_43_0_test.go index 9b6c72224..4c333e478 100644 --- a/api/datastore/migrator/migrate_2_43_0_test.go +++ b/api/datastore/migrator/migrate_2_43_0_test.go @@ -5,6 +5,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/database/boltdb" + "github.com/portainer/portainer/api/dataservices/customtemplate" "github.com/portainer/portainer/api/dataservices/source" "github.com/portainer/portainer/api/dataservices/stack" "github.com/portainer/portainer/api/dataservices/workflow" @@ -58,13 +59,13 @@ func TestMigrateGitConfigToSources_2_43_0_GitStackMigrated(t *testing.T) { wf, err := workflowSvc.Read(migrated.WorkflowID) require.NoError(t, err) require.Len(t, wf.Artifacts, 1) - require.Len(t, wf.Artifacts[0].SourceIDs, 1) + require.Len(t, wf.Artifacts[0].Files, 1) - src, err := sourceSvc.Read(wf.Artifacts[0].SourceIDs[0]) + src, err := sourceSvc.Read(wf.Artifacts[0].Files[0].SourceID) require.NoError(t, err) require.Equal(t, portainer.SourceTypeGit, src.Type) - require.Equal(t, gitStack.GitConfig.URL, src.GitConfig.URL) - require.Equal(t, gitStack.GitConfig.ReferenceName, src.GitConfig.ReferenceName) + require.Equal(t, gitStack.GitConfig.URL, src.Git.URL) + require.Equal(t, gitStack.GitConfig.ReferenceName, src.Git.ReferenceName) } func TestMigrateGitConfigToSources_2_43_0_NonGitStackUntouched(t *testing.T) { @@ -170,8 +171,8 @@ func TestMigrateGitConfigToSources_2_43_0_DuplicateSourcesDeduped(t *testing.T) sharedSourceID := sources[0].ID for _, wf := range workflows { require.Len(t, wf.Artifacts, 1) - require.Len(t, wf.Artifacts[0].SourceIDs, 1) - require.Equal(t, sharedSourceID, wf.Artifacts[0].SourceIDs[0]) + require.Len(t, wf.Artifacts[0].Files, 1) + require.Equal(t, sharedSourceID, wf.Artifacts[0].Files[0].SourceID) } } @@ -221,3 +222,241 @@ func TestMigrateGitConfigToSources_2_43_0_Idempotent(t *testing.T) { require.NoError(t, err) require.Len(t, workflows, 1) } + +func TestMigrateCustomTemplateGitConfigToSources_2_43_0_GitTemplateMigrated(t *testing.T) { + t.Parallel() + + conn := &boltdb.DbConnection{Path: t.TempDir()} + err := conn.Open() + require.NoError(t, err) + defer logs.CloseAndLogErr(conn) + + stackSvc, err := stack.NewService(conn) + require.NoError(t, err) + sourceSvc, err := source.NewService(conn) + require.NoError(t, err) + customTemplateSvc, err := customtemplate.NewService(conn) + require.NoError(t, err) + + m := NewMigrator(&MigratorParameters{ + StackService: stackSvc, + SourceService: sourceSvc, + CustomTemplateService: customTemplateSvc, + }) + + tmpl := &portainer.CustomTemplate{ + ID: 1, + GitConfig: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + ReferenceName: "refs/heads/main", + ConfigFilePath: "docker-compose.yml", + ConfigHash: "abc123", + }, + } + err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl) + require.NoError(t, err) + + err = m.migrateCustomTemplateGitConfigToSources_2_43_0() + require.NoError(t, err) + + migrated, err := customTemplateSvc.Read(tmpl.ID) + require.NoError(t, err) + require.NotNil(t, migrated.Artifact) + require.Nil(t, migrated.GitConfig) + require.Len(t, migrated.Artifact.Files, 1) + require.Equal(t, "refs/heads/main", migrated.Artifact.Files[0].Ref) + require.Equal(t, "docker-compose.yml", migrated.Artifact.Files[0].Path) + require.Equal(t, "abc123", migrated.Artifact.Files[0].Hash) + + src, err := sourceSvc.Read(migrated.Artifact.Files[0].SourceID) + require.NoError(t, err) + require.Equal(t, portainer.SourceTypeGit, src.Type) + require.Equal(t, "https://github.com/example/repo", src.Git.URL) +} + +func TestMigrateCustomTemplateGitConfigToSources_2_43_0_NonGitTemplateUntouched(t *testing.T) { + t.Parallel() + + conn := &boltdb.DbConnection{Path: t.TempDir()} + err := conn.Open() + require.NoError(t, err) + defer logs.CloseAndLogErr(conn) + + stackSvc, err := stack.NewService(conn) + require.NoError(t, err) + sourceSvc, err := source.NewService(conn) + require.NoError(t, err) + customTemplateSvc, err := customtemplate.NewService(conn) + require.NoError(t, err) + + m := NewMigrator(&MigratorParameters{ + StackService: stackSvc, + SourceService: sourceSvc, + CustomTemplateService: customTemplateSvc, + }) + + tmpl := &portainer.CustomTemplate{ID: 1, Title: "plain-template"} + err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl) + require.NoError(t, err) + + err = m.migrateCustomTemplateGitConfigToSources_2_43_0() + require.NoError(t, err) + + result, err := customTemplateSvc.Read(tmpl.ID) + require.NoError(t, err) + require.Nil(t, result.Artifact) + require.Nil(t, result.GitConfig) + + sources, err := sourceSvc.ReadAll() + require.NoError(t, err) + require.Empty(t, sources) +} + +func TestMigrateCustomTemplateGitConfigToSources_2_43_0_AlreadyMigratedSkipped(t *testing.T) { + t.Parallel() + + conn := &boltdb.DbConnection{Path: t.TempDir()} + err := conn.Open() + require.NoError(t, err) + defer logs.CloseAndLogErr(conn) + + stackSvc, err := stack.NewService(conn) + require.NoError(t, err) + sourceSvc, err := source.NewService(conn) + require.NoError(t, err) + customTemplateSvc, err := customtemplate.NewService(conn) + require.NoError(t, err) + + m := NewMigrator(&MigratorParameters{ + StackService: stackSvc, + SourceService: sourceSvc, + CustomTemplateService: customTemplateSvc, + }) + + // Template already has Artifact set (already migrated) + srcID := portainer.SourceID(99) + tmpl := &portainer.CustomTemplate{ + ID: 1, + GitConfig: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + }, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{SourceID: srcID}}, + }, + } + err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl) + require.NoError(t, err) + + err = m.migrateCustomTemplateGitConfigToSources_2_43_0() + require.NoError(t, err) + + sources, err := sourceSvc.ReadAll() + require.NoError(t, err) + require.Empty(t, sources, "no new sources should be created for already-migrated templates") +} + +func TestMigrateCustomTemplateGitConfigToSources_2_43_0_DuplicateSourcesDeduped(t *testing.T) { + t.Parallel() + + conn := &boltdb.DbConnection{Path: t.TempDir()} + err := conn.Open() + require.NoError(t, err) + defer logs.CloseAndLogErr(conn) + + stackSvc, err := stack.NewService(conn) + require.NoError(t, err) + sourceSvc, err := source.NewService(conn) + require.NoError(t, err) + customTemplateSvc, err := customtemplate.NewService(conn) + require.NoError(t, err) + + m := NewMigrator(&MigratorParameters{ + StackService: stackSvc, + SourceService: sourceSvc, + CustomTemplateService: customTemplateSvc, + }) + + sharedURL := "https://github.com/example/shared-repo" + + tmpl1 := &portainer.CustomTemplate{ + ID: 1, + Title: "template-a", + GitConfig: &gittypes.RepoConfig{ + URL: sharedURL, + ReferenceName: "refs/heads/main", + }, + } + tmpl2 := &portainer.CustomTemplate{ + ID: 2, + Title: "template-b", + GitConfig: &gittypes.RepoConfig{ + URL: sharedURL, + ReferenceName: "refs/heads/develop", + }, + } + err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl1.ID), tmpl1) + require.NoError(t, err) + err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl2.ID), tmpl2) + require.NoError(t, err) + + err = m.migrateCustomTemplateGitConfigToSources_2_43_0() + require.NoError(t, err) + + sources, err := sourceSvc.ReadAll() + require.NoError(t, err) + require.Len(t, sources, 1, "two templates with the same URL must share one Source") + + sharedSrcID := sources[0].ID + + migrated1, err := customTemplateSvc.Read(tmpl1.ID) + require.NoError(t, err) + require.NotNil(t, migrated1.Artifact) + require.Equal(t, sharedSrcID, migrated1.Artifact.Files[0].SourceID) + + migrated2, err := customTemplateSvc.Read(tmpl2.ID) + require.NoError(t, err) + require.NotNil(t, migrated2.Artifact) + require.Equal(t, sharedSrcID, migrated2.Artifact.Files[0].SourceID) +} + +func TestMigrateCustomTemplateGitConfigToSources_2_43_0_Idempotent(t *testing.T) { + t.Parallel() + + conn := &boltdb.DbConnection{Path: t.TempDir()} + err := conn.Open() + require.NoError(t, err) + defer logs.CloseAndLogErr(conn) + + stackSvc, err := stack.NewService(conn) + require.NoError(t, err) + sourceSvc, err := source.NewService(conn) + require.NoError(t, err) + customTemplateSvc, err := customtemplate.NewService(conn) + require.NoError(t, err) + + m := NewMigrator(&MigratorParameters{ + StackService: stackSvc, + SourceService: sourceSvc, + CustomTemplateService: customTemplateSvc, + }) + + tmpl := &portainer.CustomTemplate{ + ID: 1, + GitConfig: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + }, + } + err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl) + require.NoError(t, err) + + err = m.migrateCustomTemplateGitConfigToSources_2_43_0() + require.NoError(t, err) + + // Second run must not create duplicate Source records + err = m.migrateCustomTemplateGitConfigToSources_2_43_0() + require.NoError(t, err) + + sources, err := sourceSvc.ReadAll() + require.NoError(t, err) + require.Len(t, sources, 1) +} diff --git a/api/datastore/migrator/migrator.go b/api/datastore/migrator/migrator.go index 3a4bfa91c..f7fd38fdc 100644 --- a/api/datastore/migrator/migrator.go +++ b/api/datastore/migrator/migrator.go @@ -5,6 +5,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/database/models" + "github.com/portainer/portainer/api/dataservices/customtemplate" "github.com/portainer/portainer/api/dataservices/dockerhub" "github.com/portainer/portainer/api/dataservices/edgegroup" "github.com/portainer/portainer/api/dataservices/edgejob" @@ -66,6 +67,7 @@ type ( edgeGroupService *edgegroup.Service TunnelServerService *tunnelserver.Service pendingActionsService *pendingactions.Service + customTemplateService *customtemplate.Service sourceService *source.Service workflowService *workflow.Service } @@ -98,6 +100,7 @@ type ( EdgeGroupService *edgegroup.Service TunnelServerService *tunnelserver.Service PendingActionsService *pendingactions.Service + CustomTemplateService *customtemplate.Service SourceService *source.Service WorkflowService *workflow.Service } @@ -132,6 +135,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator { edgeGroupService: parameters.EdgeGroupService, TunnelServerService: parameters.TunnelServerService, pendingActionsService: parameters.PendingActionsService, + customTemplateService: parameters.CustomTemplateService, sourceService: parameters.SourceService, workflowService: parameters.WorkflowService, } @@ -268,7 +272,10 @@ func (m *Migrator) initMigrations() { m.addMigrations("2.40.0", m.migrateRegistryAccessSASecrets_2_40_0) - m.addMigrations("2.43.0", m.migrateGitConfigToSources_2_43_0) + m.addMigrations("2.43.0", + m.migrateGitConfigToSources_2_43_0, + m.migrateCustomTemplateGitConfigToSources_2_43_0, + ) // WARNING: do not change migrations that have already been released! diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index ca4cb9c2c..309a41095 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -920,7 +920,7 @@ } ], "version": { - "VERSION": "{\"SchemaVersion\":\"2.43.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" + "VERSION": "{\"SchemaVersion\":\"2.43.0\",\"MigratorCount\":2,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" }, "webhooks": null, "workflows": null diff --git a/api/docker/client/client.go b/api/docker/client/client.go index 86cc1fa4b..6ef575b75 100644 --- a/api/docker/client/client.go +++ b/api/docker/client/client.go @@ -11,6 +11,8 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/pkg/libhttp/ssrf" + "github.com/rs/zerolog/log" "github.com/docker/docker/api/types/image" @@ -184,17 +186,20 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) } func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) { - transport := &NodeNameTransport{ - Transport: &http.Transport{}, - } - + var transport *NodeNameTransport if endpoint.TLSConfig.TLS { tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig) if err != nil { return nil, err } - transport.TLSClientConfig = tlsConfig + transport = &NodeNameTransport{ + Transport: ssrf.WrapTransport(&http.Transport{TLSClientConfig: tlsConfig}), + } + } else { + transport = &NodeNameTransport{ + Transport: ssrf.WrapTransport(&http.Transport{}), + } } clientTimeout := defaultDockerRequestTimeout diff --git a/api/exec/common.go b/api/exec/common.go index 5d2b96078..593b2042c 100644 --- a/api/exec/common.go +++ b/api/exec/common.go @@ -38,7 +38,7 @@ func fetchEndpointProxy(proxyManager *proxy.Manager, endpoint *portainer.Endpoin // portainerRegistriesToAuthConfigs converts registries to Docker auth configs. // Callers must ensure ECR tokens are valid before calling this function (e.g. via -// registryutils.ValidateRegistriesECRTokens with a real DataStoreTx). This function +// registryutils.RefreshAndPersistECRTokens with a real DataStoreTx). This function // intentionally performs no DB writes to avoid write-lock contention when called inside // an active BoltDB write transaction. func portainerRegistriesToAuthConfigs(registries []portainer.Registry) []types.AuthConfig { diff --git a/api/git/azure.go b/api/git/azure.go index 79e5c340c..2acf14b23 100644 --- a/api/git/azure.go +++ b/api/git/azure.go @@ -14,12 +14,13 @@ import ( "github.com/portainer/portainer/api/crypto" gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/logs" - "github.com/rs/zerolog/log" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/filemode" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/segmentio/encoding/json" ) @@ -64,15 +65,13 @@ func NewAzureClient() *azureClient { } func newHttpClientForAzure(insecureSkipVerify bool) *http.Client { - httpsCli := &http.Client{ - Transport: &http.Transport{ + return &http.Client{ + Transport: ssrf.WrapTransport(&http.Transport{ TLSClientConfig: crypto.CreateTLSConfiguration(insecureSkipVerify), Proxy: http.ProxyFromEnvironment, - }, + }), Timeout: 300 * time.Second, } - - return httpsCli } func (a *azureClient) Download(ctx context.Context, destination string, opt *git.CloneOptions) error { diff --git a/api/gitops/workflows/fetch.go b/api/gitops/workflows/fetch.go index f657498ba..a8a018845 100644 --- a/api/gitops/workflows/fetch.go +++ b/api/gitops/workflows/fetch.go @@ -9,8 +9,6 @@ import ( "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/set" - - "github.com/rs/zerolog/log" ) // FetchWorkflows returns all GitOps workflows visible to the given user. @@ -43,69 +41,35 @@ func FetchWorkflows( // First pass: filter by endpoint/stack-type match and collect workflow IDs. preFiltered := make([]portainer.Stack, 0, len(stacks)) - workflowIDSet := make(map[portainer.WorkflowID]struct{}, len(stacks)) + workflowIDSet := make(set.Set[portainer.WorkflowID], len(stacks)) for _, stack := range stacks { if ep, ok := endpointMap[stack.EndpointID]; ok && !EndpointMatchesStackType(ep, stack.Type) { continue } preFiltered = append(preFiltered, stack) - workflowIDSet[stack.WorkflowID] = struct{}{} + workflowIDSet.Add(stack.WorkflowID) } - // Batch-load all needed workflows in one scan. - wfs, err := tx.Workflow().ReadAll(func(wf portainer.Workflow) bool { - _, ok := workflowIDSet[wf.ID] - return ok - }) + workflowMap, sourceMap, err := LoadWorkflowAndSourceMaps(tx, workflowIDSet) if err != nil { return nil, err } - workflowMap := make(map[portainer.WorkflowID]portainer.Workflow, len(wfs)) - var allArtifacts []portainer.ArtifactSources - for _, wf := range wfs { - workflowMap[wf.ID] = wf - allArtifacts = append(allArtifacts, wf.Artifacts...) - } - sourceSet := ArtifactsToSourceSet(allArtifacts...) - - // Batch-load all needed sources in one scan. - srcs, err := tx.Source().ReadAll(func(src portainer.Source) bool { - return sourceSet.Contains(src.ID) - }) - if err != nil { - return nil, err - } - - sourceMap := make(map[portainer.SourceID]portainer.Source, len(srcs)) - for _, src := range srcs { - sourceMap[src.ID] = src - } - // Second pass: build filtered list using in-memory lookups. var filtered []portainer.Stack for _, stack := range preFiltered { - wf, ok := workflowMap[stack.WorkflowID] - if !ok { - log.Warn().Int("stackID", int(stack.ID)).Msg("workflow record missing for stack, skipping") - continue - } + wf := workflowMap[stack.WorkflowID] outer: for _, as := range wf.Artifacts { - if as.Artifact.StackID != stack.ID { + if as.StackID != stack.ID { continue } - for _, srcID := range as.SourceIDs { - src, ok := sourceMap[srcID] - if !ok { - log.Warn().Int("stackID", int(stack.ID)).Msg("source record missing for stack, skipping") - break outer - } - + for _, f := range as.Files { + src := sourceMap[f.SourceID] if src.Type == portainer.SourceTypeGit { - gitConfigs[stack.ID] = MergeSourceAndArtifact(&src, &as.Artifact) + gitConfigs[stack.ID] = MergeSourceAndFile(&src, &f) break outer } } @@ -169,28 +133,27 @@ func FetchSourceStats( return nil, nil, err } - workflowIDSet := make(map[portainer.WorkflowID]struct{}, len(allStacks)) + workflowIDSet := make(set.Set[portainer.WorkflowID], len(allStacks)) preFiltered := make([]portainer.Stack, 0, len(allStacks)) for _, stack := range allStacks { if ep, ok := endpointMap[stack.EndpointID]; ok && !EndpointMatchesStackType(ep, stack.Type) { continue } preFiltered = append(preFiltered, stack) - workflowIDSet[stack.WorkflowID] = struct{}{} + workflowIDSet.Add(stack.WorkflowID) } - wfs, err := tx.Workflow().ReadAll(func(wf portainer.Workflow) bool { - _, ok := workflowIDSet[wf.ID] - return ok - }) + wfMap, err := LoadWorkflowMap(tx, workflowIDSet) if err != nil { return nil, nil, err } - wfSources := make(map[portainer.WorkflowID][]portainer.SourceID, len(wfs)) - for _, wf := range wfs { + wfSources := make(map[portainer.WorkflowID][]portainer.SourceID, len(wfMap)) + for id, wf := range wfMap { for _, as := range wf.Artifacts { - wfSources[wf.ID] = append(wfSources[wf.ID], as.SourceIDs...) + for _, f := range as.Files { + wfSources[id] = append(wfSources[id], f.SourceID) + } } } diff --git a/api/gitops/workflows/fetch_test.go b/api/gitops/workflows/fetch_test.go index fdfbedd26..481eca78d 100644 --- a/api/gitops/workflows/fetch_test.go +++ b/api/gitops/workflows/fetch_test.go @@ -23,12 +23,12 @@ func mustCreateGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *por cfg := stack.GitConfig - src := &portainer.Source{Type: portainer.SourceTypeGit, GitConfig: cfg} + src := &portainer.Source{Type: portainer.SourceTypeGit, Git: cfg} require.NoError(t, tx.Source().Create(src)) - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: stack.ID}, - SourceIDs: []portainer.SourceID{src.ID}, + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{ + StackID: stack.ID, + Files: []portainer.ArtifactFile{{SourceID: src.ID}}, }}} require.NoError(t, tx.Workflow().Create(wf)) @@ -228,7 +228,7 @@ func TestFetchSourceStats_TracksWorkflowCountAndEndpoints(t *testing.T) { srcID = src.ID for i := 1; i <= 2; i++ { - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{srcID}}}} + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: srcID}}}}} require.NoError(t, tx.Workflow().Create(wf)) require.NoError(t, tx.Stack().Create(&portainer.Stack{ ID: portainer.StackID(i), diff --git a/api/gitops/workflows/mapping.go b/api/gitops/workflows/mapping.go index fec1c70ec..df0eca4be 100644 --- a/api/gitops/workflows/mapping.go +++ b/api/gitops/workflows/mapping.go @@ -1,6 +1,8 @@ package workflows import ( + "slices" + portainer "github.com/portainer/portainer/api" gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/set" @@ -61,9 +63,9 @@ func MapEdgeStackToWorkflow(es portainer.EdgeStack, gitConfig *gittypes.RepoConf } func StackLastSyncDate(s portainer.Stack) int64 { - for i := len(s.DeploymentStatus) - 1; i >= 0; i-- { - if s.DeploymentStatus[i].Status == portainer.StackStatusActive { - return s.DeploymentStatus[i].Time + for _, ds := range slices.Backward(s.DeploymentStatus) { + if ds.Status == portainer.StackStatusActive { + return ds.Time } } return 0 @@ -84,9 +86,9 @@ func edgeStackLastSyncDate(statuses []portainer.EdgeStackStatusForEnv) int64 { } func endpointLastSyncDate(epStatus portainer.EdgeStackStatusForEnv) int64 { - for i := len(epStatus.Status) - 1; i >= 0; i-- { - if isEdgeStackHealthyStatus(epStatus.Status[i].Type) { - return epStatus.Status[i].Time + for _, s := range slices.Backward(epStatus.Status) { + if isEdgeStackHealthyStatus(s.Type) { + return s.Time } } return 0 @@ -115,18 +117,6 @@ func isEdgeStackHealthyStatus(t portainer.EdgeStackStatusType) bool { return false } -// ArtifactsToSourceSet returns the set of all SourceIDs referenced by the given artifact-source mappings -func ArtifactsToSourceSet(artifacts ...portainer.ArtifactSources) set.Set[portainer.SourceID] { - s := make(set.Set[portainer.SourceID]) - for _, a := range artifacts { - for _, sid := range a.SourceIDs { - s.Add(sid) - } - } - - return s -} - func resolveEdgeGroupEndpoints(groups []portainer.EdgeGroupID, groupEndpoints map[portainer.EdgeGroupID][]portainer.EndpointID) []portainer.EndpointID { seen := set.Set[portainer.EndpointID]{} for _, gid := range groups { diff --git a/api/gitops/workflows/source_artifact.go b/api/gitops/workflows/source_artifact.go index e42ac66ad..c0681707b 100644 --- a/api/gitops/workflows/source_artifact.go +++ b/api/gitops/workflows/source_artifact.go @@ -6,6 +6,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" gittypes "github.com/portainer/portainer/api/git/types" + "github.com/portainer/portainer/api/set" ) // gitSourceStore is the minimal intersection of CE and EE DataStoreTx that these functions need. @@ -14,11 +15,11 @@ type gitSourceStore interface { Source() dataservices.SourceService } -// GitSourceAndArtifactForStack returns the git Source and the Artifact matching stackID +// GitSourceAndArtifactForStack returns the git Source and the ArtifactFile matching stackID // from the workflow identified by workflowID. -// Source carries the shared fields (URL, auth, TLS); Artifact carries the stack-specific fields (ref, path, hash). +// Source carries the shared fields (URL, auth, TLS); ArtifactFile carries the file-specific fields (ref, path, hash). // Returns nil, nil, nil when workflowID is 0 or no matching entry is found. -func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID) (*portainer.Source, *portainer.Artifact, error) { +func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID) (*portainer.Source, *portainer.ArtifactFile, error) { if workflowID == 0 { return nil, nil, nil } @@ -28,19 +29,24 @@ func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.Workfl return nil, nil, err } + sourceMap, err := loadWorkflowSources(tx, wf) + if err != nil { + return nil, nil, err + } + for i, as := range wf.Artifacts { - if as.Artifact.StackID != stackID { + if as.StackID != stackID { continue } - for _, srcID := range as.SourceIDs { - src, err := tx.Source().Read(srcID) - if err != nil { - return nil, nil, err + for j, file := range as.Files { + src, ok := sourceMap[file.SourceID] + if !ok { + continue } if src.Type == portainer.SourceTypeGit { - return src, &wf.Artifacts[i].Artifact, nil + return &src, &wf.Artifacts[i].Files[j], nil } } } @@ -48,9 +54,9 @@ func GitSourceAndArtifactForStack(tx gitSourceStore, workflowID portainer.Workfl return nil, nil, nil } -// GitSourceAndArtifactForEdgeStack returns the git Source and the Artifact matching edgeStackID. +// GitSourceAndArtifactForEdgeStack returns the git Source and the ArtifactFile matching edgeStackID. // Returns nil, nil, nil when workflowID is 0 or no matching entry is found. -func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID) (*portainer.Source, *portainer.Artifact, error) { +func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID) (*portainer.Source, *portainer.ArtifactFile, error) { if workflowID == 0 { return nil, nil, nil } @@ -60,19 +66,24 @@ func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Wo return nil, nil, err } + sourceMap, err := loadWorkflowSources(tx, wf) + if err != nil { + return nil, nil, err + } + for i, as := range wf.Artifacts { - if as.Artifact.EdgeStackID != edgeStackID { + if as.EdgeStackID != edgeStackID { continue } - for _, srcID := range as.SourceIDs { - src, err := tx.Source().Read(srcID) - if err != nil { - return nil, nil, err + for j, file := range as.Files { + src, ok := sourceMap[file.SourceID] + if !ok { + continue } if src.Type == portainer.SourceTypeGit { - return src, &wf.Artifacts[i].Artifact, nil + return &src, &wf.Artifacts[i].Files[j], nil } } } @@ -80,60 +91,74 @@ func GitSourceAndArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Wo return nil, nil, nil } -// MergeSourceAndArtifact builds a RepoConfig by combining shared fields from src (URL, auth, TLS) -// with stack-specific fields from artifact (ref, path, hash). -func MergeSourceAndArtifact(src *portainer.Source, artifact *portainer.Artifact) *gittypes.RepoConfig { - if src == nil || src.GitConfig == nil { +// MergeSourceAndFile builds a RepoConfig by combining shared fields from src (URL, auth, TLS) +// with file-specific fields from file (ref, path, hash). +func MergeSourceAndFile(src *portainer.Source, file *portainer.ArtifactFile) *gittypes.RepoConfig { + if src == nil || src.Git == nil { return nil } cfg := &gittypes.RepoConfig{ - URL: src.GitConfig.URL, - Authentication: src.GitConfig.Authentication, - TLSSkipVerify: src.GitConfig.TLSSkipVerify, + URL: src.Git.URL, + Authentication: src.Git.Authentication, + TLSSkipVerify: src.Git.TLSSkipVerify, } - if artifact != nil { - cfg.ReferenceName = artifact.ReferenceName - cfg.ConfigFilePath = artifact.ConfigFilePath - cfg.ConfigHash = artifact.ConfigHash + if file != nil { + cfg.ReferenceName = file.Ref + cfg.ConfigFilePath = file.Path + cfg.ConfigHash = file.Hash } return cfg } -// UpdateArtifactForStack finds the Artifact matching stackID in the workflow and applies fn to it, -// then persists the updated Workflow. A no-op if no matching Artifact is found. -func UpdateArtifactForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID, fn func(*portainer.Artifact)) error { +// UpdateArtifactFileForStack finds the ArtifactFile matching stackID and sourceID in the workflow +// and applies fn to it, then persists the updated Workflow. +// A no-op if no matching artifact or file is found. +func UpdateArtifactFileForStack(tx gitSourceStore, workflowID portainer.WorkflowID, stackID portainer.StackID, sourceID portainer.SourceID, fn func(*portainer.ArtifactFile)) error { wf, err := tx.Workflow().Read(workflowID) if err != nil { return err } for i, as := range wf.Artifacts { - if as.Artifact.StackID == stackID { - fn(&wf.Artifacts[i].Artifact) + if as.StackID != stackID { + continue + } - return tx.Workflow().Update(workflowID, wf) + for j, file := range as.Files { + if file.SourceID == sourceID { + fn(&wf.Artifacts[i].Files[j]) + + return tx.Workflow().Update(workflowID, wf) + } } } return nil } -// UpdateArtifactForEdgeStack finds the Artifact matching edgeStackID in the workflow and applies fn to it, -// then persists the updated Workflow. A no-op if no matching Artifact is found. -func UpdateArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID, fn func(*portainer.Artifact)) error { +// UpdateArtifactFileForEdgeStack finds the ArtifactFile matching edgeStackID and sourceID in the workflow +// and applies fn to it, then persists the updated Workflow. +// A no-op if no matching artifact or file is found. +func UpdateArtifactFileForEdgeStack(tx gitSourceStore, workflowID portainer.WorkflowID, edgeStackID portainer.EdgeStackID, sourceID portainer.SourceID, fn func(*portainer.ArtifactFile)) error { wf, err := tx.Workflow().Read(workflowID) if err != nil { return err } for i, as := range wf.Artifacts { - if as.Artifact.EdgeStackID == edgeStackID { - fn(&wf.Artifacts[i].Artifact) + if as.EdgeStackID != edgeStackID { + continue + } - return tx.Workflow().Update(workflowID, wf) + for j, file := range as.Files { + if file.SourceID == sourceID { + fn(&wf.Artifacts[i].Files[j]) + + return tx.Workflow().Update(workflowID, wf) + } } } @@ -144,13 +169,13 @@ func UpdateArtifactForEdgeStack(tx gitSourceStore, workflowID portainer.Workflow // or creates a new one. Only URL, authentication, and TLSSkipVerify are stored on the Source; // per-stack fields (ReferenceName, ConfigFilePath, ConfigHash) belong in the Artifact. func FindOrCreateGitSource(tx gitSourceStore, src *portainer.Source) (*portainer.Source, error) { - src.GitConfig.URL = gittypes.SanitizeURL(src.GitConfig.URL) + src.Git.URL = gittypes.SanitizeURL(src.Git.URL) existing, err := tx.Source().ReadAll(func(s portainer.Source) bool { return s.Type == portainer.SourceTypeGit && - s.GitConfig != nil && - s.GitConfig.URL == src.GitConfig.URL && - gitAuthMatches(s.GitConfig.Authentication, src.GitConfig.Authentication) + s.Git != nil && + s.Git.URL == src.Git.URL && + gitAuthMatches(s.Git.Authentication, src.Git.Authentication) }) if err != nil { return nil, err @@ -163,10 +188,10 @@ func FindOrCreateGitSource(tx gitSourceStore, src *portainer.Source) (*portainer toCreate := &portainer.Source{ Name: src.Name, Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ - URL: src.GitConfig.URL, - Authentication: src.GitConfig.Authentication, - TLSSkipVerify: src.GitConfig.TLSSkipVerify, + Git: &gittypes.RepoConfig{ + URL: src.Git.URL, + Authentication: src.Git.Authentication, + TLSSkipVerify: src.Git.TLSSkipVerify, }, } @@ -186,17 +211,17 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m return fmt.Errorf("failed to read source: %w", err) } - if src.GitConfig == nil { + if src.Git == nil { return fmt.Errorf("source %d has no git configuration", oldSourceID) } newSourceID := oldSourceID - if cfg.URL != src.GitConfig.URL { + if cfg.URL != src.Git.URL { newSrc, err := FindOrCreateGitSource(tx, &portainer.Source{ - Name: gittypes.RepoName(cfg.URL), - Type: portainer.SourceTypeGit, - GitConfig: cfg, + Name: gittypes.RepoName(cfg.URL), + Type: portainer.SourceTypeGit, + Git: cfg, }) if err != nil { return fmt.Errorf("failed to find or create source: %w", err) @@ -204,8 +229,8 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m newSourceID = newSrc.ID } else { - src.GitConfig.Authentication = cfg.Authentication - src.GitConfig.TLSSkipVerify = cfg.TLSSkipVerify + src.Git.Authentication = cfg.Authentication + src.Git.TLSSkipVerify = cfg.TLSSkipVerify if err := tx.Source().Update(src.ID, src); err != nil { return fmt.Errorf("failed to update source: %w", err) @@ -218,20 +243,24 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m } for i, as := range wf.Artifacts { - if !matchArtifact(as.Artifact) { + if !matchArtifact(as) { continue } - wf.Artifacts[i].Artifact.ReferenceName = cfg.ReferenceName - wf.Artifacts[i].Artifact.ConfigFilePath = cfg.ConfigFilePath - wf.Artifacts[i].Artifact.ConfigHash = cfg.ConfigHash - - if newSourceID != oldSourceID { - for j, sID := range as.SourceIDs { - if sID == oldSourceID { - wf.Artifacts[i].SourceIDs[j] = newSourceID - } + for j, file := range as.Files { + if file.SourceID != oldSourceID { + continue } + + wf.Artifacts[i].Files[j].Ref = cfg.ReferenceName + wf.Artifacts[i].Files[j].Path = cfg.ConfigFilePath + wf.Artifacts[i].Files[j].Hash = cfg.ConfigHash + + if newSourceID != oldSourceID { + wf.Artifacts[i].Files[j].SourceID = newSourceID + } + + break } break @@ -240,6 +269,73 @@ func SaveWorkflowGitConfig(tx gitSourceStore, workflowID portainer.WorkflowID, m return tx.Workflow().Update(workflowID, wf) } +// LoadWorkflowMap fetches workflows by their IDs and returns them keyed by ID. +func LoadWorkflowMap(tx gitSourceStore, ids set.Set[portainer.WorkflowID]) (map[portainer.WorkflowID]portainer.Workflow, error) { + result := make(map[portainer.WorkflowID]portainer.Workflow, len(ids)) + for id := range ids { + wf, err := tx.Workflow().Read(id) + if err != nil { + return nil, err + } + result[id] = *wf + } + + return result, nil +} + +// LoadWorkflowAndSourceMaps fetches workflows by their IDs and the sources they reference, +// collecting source IDs in a single pass over the workflows. +func LoadWorkflowAndSourceMaps(tx gitSourceStore, ids set.Set[portainer.WorkflowID]) (map[portainer.WorkflowID]portainer.Workflow, map[portainer.SourceID]portainer.Source, error) { + wfMap := make(map[portainer.WorkflowID]portainer.Workflow, len(ids)) + sourceIDs := make(set.Set[portainer.SourceID]) + for id := range ids { + wf, err := tx.Workflow().Read(id) + if err != nil { + return nil, nil, err + } + wfMap[id] = *wf + for _, as := range wf.Artifacts { + for _, f := range as.Files { + sourceIDs.Add(f.SourceID) + } + } + } + + srcMap, err := LoadSourceMap(tx, sourceIDs) + if err != nil { + return nil, nil, err + } + + return wfMap, srcMap, nil +} + +// loadWorkflowSources collects all unique SourceIDs referenced by wf and returns them as a map. +// This avoids reading the same Source record more than once when files share a SourceID. +func loadWorkflowSources(tx gitSourceStore, wf *portainer.Workflow) (map[portainer.SourceID]portainer.Source, error) { + ids := make(set.Set[portainer.SourceID]) + for _, as := range wf.Artifacts { + for _, f := range as.Files { + ids.Add(f.SourceID) + } + } + + return LoadSourceMap(tx, ids) +} + +// LoadSourceMap fetches sources by their IDs and returns them keyed by ID. +func LoadSourceMap(tx gitSourceStore, ids set.Set[portainer.SourceID]) (map[portainer.SourceID]portainer.Source, error) { + result := make(map[portainer.SourceID]portainer.Source, len(ids)) + for id := range ids { + src, err := tx.Source().Read(id) + if err != nil { + return nil, err + } + result[id] = *src + } + + return result, nil +} + func gitAuthMatches(a, b *gittypes.GitAuthentication) bool { if a == nil && b == nil { return true @@ -260,11 +356,11 @@ func ValidateUniqueSourceURL(tx gitSourceStore, url string, sourceID portainer.S } existing, err := tx.Source().ReadAll(func(s portainer.Source) bool { - if s.ID == sourceID || s.Type != portainer.SourceTypeGit || s.GitConfig == nil { + if s.ID == sourceID || s.Type != portainer.SourceTypeGit || s.Git == nil { return false } - normalized, err := gittypes.NormalizeURL(gittypes.SanitizeURL(s.GitConfig.URL)) + normalized, err := gittypes.NormalizeURL(gittypes.SanitizeURL(s.Git.URL)) return err == nil && normalized == normalizedURL }) diff --git a/api/gitops/workflows/source_artifact_test.go b/api/gitops/workflows/source_artifact_test.go index c7173bee9..58b96936d 100644 --- a/api/gitops/workflows/source_artifact_test.go +++ b/api/gitops/workflows/source_artifact_test.go @@ -11,24 +11,24 @@ import ( "github.com/stretchr/testify/require" ) -func TestMergeSourceAndArtifact_NilSourceReturnsNil(t *testing.T) { +func TestMergeSourceAndFile_NilSourceReturnsNil(t *testing.T) { t.Parallel() - require.Nil(t, MergeSourceAndArtifact(nil, nil)) + require.Nil(t, MergeSourceAndFile(nil, nil)) } -func TestMergeSourceAndArtifact_NilGitConfigReturnsNil(t *testing.T) { +func TestMergeSourceAndFile_NilGitConfigReturnsNil(t *testing.T) { t.Parallel() src := &portainer.Source{Type: portainer.SourceTypeGit} - require.Nil(t, MergeSourceAndArtifact(src, nil)) + require.Nil(t, MergeSourceAndFile(src, nil)) } -func TestMergeSourceAndArtifact_NilArtifactLeavesPerStackFieldsEmpty(t *testing.T) { +func TestMergeSourceAndFile_NilFileLeaveFileFieldsEmpty(t *testing.T) { t.Parallel() src := &portainer.Source{ - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", TLSSkipVerify: true, Authentication: &gittypes.GitAuthentication{ @@ -38,7 +38,7 @@ func TestMergeSourceAndArtifact_NilArtifactLeavesPerStackFieldsEmpty(t *testing. }, } - cfg := MergeSourceAndArtifact(src, nil) + cfg := MergeSourceAndFile(src, nil) require.NotNil(t, cfg) require.Equal(t, "https://github.com/example/repo", cfg.URL) require.True(t, cfg.TLSSkipVerify) @@ -48,22 +48,22 @@ func TestMergeSourceAndArtifact_NilArtifactLeavesPerStackFieldsEmpty(t *testing. require.Empty(t, cfg.ConfigHash) } -func TestMergeSourceAndArtifact_MergesAllFieldsFromArtifact(t *testing.T) { +func TestMergeSourceAndFile_MergesAllFieldsFromFile(t *testing.T) { t.Parallel() src := &portainer.Source{ - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", TLSSkipVerify: true, }, } - artifact := &portainer.Artifact{ - ReferenceName: "refs/heads/main", - ConfigFilePath: "docker-compose.yml", - ConfigHash: "abc123", + file := &portainer.ArtifactFile{ + Path: "docker-compose.yml", + Ref: "refs/heads/main", + Hash: "abc123", } - cfg := MergeSourceAndArtifact(src, artifact) + cfg := MergeSourceAndFile(src, file) require.NotNil(t, cfg) require.Equal(t, "https://github.com/example/repo", cfg.URL) require.True(t, cfg.TLSSkipVerify) @@ -77,39 +77,39 @@ func TestGitSourceAndArtifactForStack_ZeroWorkflowIDReturnsNil(t *testing.T) { _, store := datastore.MustNewTestStore(t, false, true) var src *portainer.Source - var artifact *portainer.Artifact + var file *portainer.ArtifactFile err := store.ViewTx(func(tx dataservices.DataStoreTx) error { var txErr error - src, artifact, txErr = GitSourceAndArtifactForStack(tx, 0, 1) + src, file, txErr = GitSourceAndArtifactForStack(tx, 0, 1) return txErr }) require.NoError(t, err) require.Nil(t, src) - require.Nil(t, artifact) + require.Nil(t, file) } -func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndArtifact(t *testing.T) { +func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndFile(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) var workflowID portainer.WorkflowID err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { gitSrc := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, } err := tx.Source().Create(gitSrc) require.NoError(t, err) wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - StackID: 42, - ReferenceName: "refs/heads/main", - ConfigFilePath: "docker-compose.yml", - ConfigHash: "abc123", - }, - SourceIDs: []portainer.SourceID{gitSrc.ID}, + Artifacts: []portainer.Artifact{{ + StackID: 42, + Files: []portainer.ArtifactFile{{ + SourceID: gitSrc.ID, + Path: "docker-compose.yml", + Ref: "refs/heads/main", + Hash: "abc123", + }}, }}, } err = tx.Workflow().Create(wf) @@ -121,20 +121,19 @@ func TestGitSourceAndArtifactForStack_ReturnsMatchingSourceAndArtifact(t *testin require.NoError(t, err) var src *portainer.Source - var artifact *portainer.Artifact + var file *portainer.ArtifactFile err = store.ViewTx(func(tx dataservices.DataStoreTx) error { var txErr error - src, artifact, txErr = GitSourceAndArtifactForStack(tx, workflowID, 42) + src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 42) return txErr }) require.NoError(t, err) require.NotNil(t, src) require.Equal(t, portainer.SourceTypeGit, src.Type) - require.NotNil(t, artifact) - require.Equal(t, portainer.StackID(42), artifact.StackID) - require.Equal(t, "refs/heads/main", artifact.ReferenceName) - require.Equal(t, "docker-compose.yml", artifact.ConfigFilePath) - require.Equal(t, "abc123", artifact.ConfigHash) + require.NotNil(t, file) + require.Equal(t, "refs/heads/main", file.Ref) + require.Equal(t, "docker-compose.yml", file.Path) + require.Equal(t, "abc123", file.Hash) } func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T) { @@ -144,16 +143,16 @@ func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T) var workflowID portainer.WorkflowID err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { src := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, } err := tx.Source().Create(src) require.NoError(t, err) wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1}, - SourceIDs: []portainer.SourceID{src.ID}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: src.ID}}, }}, } err = tx.Workflow().Create(wf) @@ -165,15 +164,15 @@ func TestGitSourceAndArtifactForStack_NoMatchingArtifactReturnsNil(t *testing.T) require.NoError(t, err) var src *portainer.Source - var artifact *portainer.Artifact + var file *portainer.ArtifactFile err = store.ViewTx(func(tx dataservices.DataStoreTx) error { var txErr error - src, artifact, txErr = GitSourceAndArtifactForStack(tx, workflowID, 99) + src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 99) return txErr }) require.NoError(t, err) require.Nil(t, src) - require.Nil(t, artifact) + require.Nil(t, file) } func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) { @@ -187,9 +186,9 @@ func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) { require.NoError(t, err) wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1}, - SourceIDs: []portainer.SourceID{nonGitSrc.ID}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: nonGitSrc.ID}}, }}, } err = tx.Workflow().Create(wf) @@ -201,15 +200,15 @@ func TestGitSourceAndArtifactForStack_NonGitSourceSkipped(t *testing.T) { require.NoError(t, err) var src *portainer.Source - var artifact *portainer.Artifact + var file *portainer.ArtifactFile err = store.ViewTx(func(tx dataservices.DataStoreTx) error { var txErr error - src, artifact, txErr = GitSourceAndArtifactForStack(tx, workflowID, 1) + src, file, txErr = GitSourceAndArtifactForStack(tx, workflowID, 1) return txErr }) require.NoError(t, err) require.Nil(t, src) - require.Nil(t, artifact) + require.Nil(t, file) } func TestGitSourceAndArtifactForEdgeStack_ZeroWorkflowIDReturnsNil(t *testing.T) { @@ -217,38 +216,38 @@ func TestGitSourceAndArtifactForEdgeStack_ZeroWorkflowIDReturnsNil(t *testing.T) _, store := datastore.MustNewTestStore(t, false, true) var src *portainer.Source - var artifact *portainer.Artifact + var file *portainer.ArtifactFile err := store.ViewTx(func(tx dataservices.DataStoreTx) error { var txErr error - src, artifact, txErr = GitSourceAndArtifactForEdgeStack(tx, 0, 1) + src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, 0, 1) return txErr }) require.NoError(t, err) require.Nil(t, src) - require.Nil(t, artifact) + require.Nil(t, file) } -func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndArtifact(t *testing.T) { +func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndFile(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) var workflowID portainer.WorkflowID err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { gitSrc := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/edge-repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/edge-repo"}, } err := tx.Source().Create(gitSrc) require.NoError(t, err) wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - EdgeStackID: 5, - ReferenceName: "refs/heads/edge", - ConfigFilePath: "edge.yml", - }, - SourceIDs: []portainer.SourceID{gitSrc.ID}, + Artifacts: []portainer.Artifact{{ + EdgeStackID: 5, + Files: []portainer.ArtifactFile{{ + SourceID: gitSrc.ID, + Path: "edge.yml", + Ref: "refs/heads/edge", + }}, }}, } err = tx.Workflow().Create(wf) @@ -260,32 +259,38 @@ func TestGitSourceAndArtifactForEdgeStack_ReturnsMatchingSourceAndArtifact(t *te require.NoError(t, err) var src *portainer.Source - var artifact *portainer.Artifact + var file *portainer.ArtifactFile err = store.ViewTx(func(tx dataservices.DataStoreTx) error { var txErr error - src, artifact, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 5) + src, file, txErr = GitSourceAndArtifactForEdgeStack(tx, workflowID, 5) return txErr }) require.NoError(t, err) require.NotNil(t, src) require.Equal(t, portainer.SourceTypeGit, src.Type) - require.NotNil(t, artifact) - require.Equal(t, portainer.EdgeStackID(5), artifact.EdgeStackID) - require.Equal(t, "refs/heads/edge", artifact.ReferenceName) + require.NotNil(t, file) + require.Equal(t, "refs/heads/edge", file.Ref) } -func TestUpdateArtifactForStack_NoMatchingArtifactIsNoOp(t *testing.T) { +func TestUpdateArtifactFileForStack_NoMatchingArtifactIsNoOp(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) var workflowID portainer.WorkflowID + var sourceID portainer.SourceID err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}} + err := tx.Source().Create(src) + require.NoError(t, err) + sourceID = src.ID + wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1, ConfigHash: "original"}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "original"}}, }}, } - err := tx.Workflow().Create(wf) + err = tx.Workflow().Create(wf) require.NoError(t, err) workflowID = wf.ID @@ -294,29 +299,36 @@ func TestUpdateArtifactForStack_NoMatchingArtifactIsNoOp(t *testing.T) { require.NoError(t, err) err = store.UpdateTx(func(tx dataservices.DataStoreTx) error { - return UpdateArtifactForStack(tx, workflowID, 99, func(a *portainer.Artifact) { - a.ConfigHash = "changed" + return UpdateArtifactFileForStack(tx, workflowID, 99, sourceID, func(a *portainer.ArtifactFile) { + a.Hash = "changed" }) }) require.NoError(t, err) wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - require.Equal(t, "original", wf.Artifacts[0].Artifact.ConfigHash) + require.Equal(t, "original", wf.Artifacts[0].Files[0].Hash) } -func TestUpdateArtifactForStack_AppliesFnAndPersists(t *testing.T) { +func TestUpdateArtifactFileForStack_AppliesFnAndPersists(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) var workflowID portainer.WorkflowID + var sourceID portainer.SourceID err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}} + err := tx.Source().Create(src) + require.NoError(t, err) + sourceID = src.ID + wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1, ConfigHash: "old-hash"}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "old-hash"}}, }}, } - err := tx.Workflow().Create(wf) + err = tx.Workflow().Create(wf) require.NoError(t, err) workflowID = wf.ID @@ -325,29 +337,36 @@ func TestUpdateArtifactForStack_AppliesFnAndPersists(t *testing.T) { require.NoError(t, err) err = store.UpdateTx(func(tx dataservices.DataStoreTx) error { - return UpdateArtifactForStack(tx, workflowID, 1, func(a *portainer.Artifact) { - a.ConfigHash = "new-hash" + return UpdateArtifactFileForStack(tx, workflowID, 1, sourceID, func(a *portainer.ArtifactFile) { + a.Hash = "new-hash" }) }) require.NoError(t, err) wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - require.Equal(t, "new-hash", wf.Artifacts[0].Artifact.ConfigHash) + require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash) } -func TestUpdateArtifactForEdgeStack_AppliesFnAndPersists(t *testing.T) { +func TestUpdateArtifactFileForEdgeStack_AppliesFnAndPersists(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) var workflowID portainer.WorkflowID + var sourceID portainer.SourceID err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{Type: portainer.SourceTypeGit, Git: &gittypes.RepoConfig{URL: "https://example.com"}} + err := tx.Source().Create(src) + require.NoError(t, err) + sourceID = src.ID + wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{EdgeStackID: 7, ConfigHash: "old-hash"}, + Artifacts: []portainer.Artifact{{ + EdgeStackID: 7, + Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "old-hash"}}, }}, } - err := tx.Workflow().Create(wf) + err = tx.Workflow().Create(wf) require.NoError(t, err) workflowID = wf.ID @@ -356,15 +375,15 @@ func TestUpdateArtifactForEdgeStack_AppliesFnAndPersists(t *testing.T) { require.NoError(t, err) err = store.UpdateTx(func(tx dataservices.DataStoreTx) error { - return UpdateArtifactForEdgeStack(tx, workflowID, 7, func(a *portainer.Artifact) { - a.ConfigHash = "new-hash" + return UpdateArtifactFileForEdgeStack(tx, workflowID, 7, sourceID, func(a *portainer.ArtifactFile) { + a.Hash = "new-hash" }) }) require.NoError(t, err) wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - require.Equal(t, "new-hash", wf.Artifacts[0].Artifact.ConfigHash) + require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash) } func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) { @@ -377,7 +396,7 @@ func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) { src, txErr = FindOrCreateGitSource(tx, &portainer.Source{ Name: "my-repo", Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", }, }) @@ -386,7 +405,7 @@ func TestFindOrCreateGitSource_CreatesNewSource(t *testing.T) { require.NoError(t, err) require.NotNil(t, src) require.NotZero(t, src.ID) - require.Equal(t, "https://github.com/example/repo", src.GitConfig.URL) + require.Equal(t, "https://github.com/example/repo", src.Git.URL) } func TestFindOrCreateGitSource_ReusesExistingSourceForSameURLAndAuth(t *testing.T) { @@ -396,7 +415,7 @@ func TestFindOrCreateGitSource_ReusesExistingSourceForSameURLAndAuth(t *testing. makeSource := func(tx dataservices.DataStoreTx) (*portainer.Source, error) { return FindOrCreateGitSource(tx, &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", }, }) @@ -439,7 +458,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) { err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { _, txErr := FindOrCreateGitSource(tx, &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", Authentication: &gittypes.GitAuthentication{Username: "alice", Password: "pass1"}, }, @@ -451,7 +470,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) { err = store.UpdateTx(func(tx dataservices.DataStoreTx) error { _, txErr := FindOrCreateGitSource(tx, &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", Authentication: &gittypes.GitAuthentication{Username: "bob", Password: "pass2"}, }, @@ -465,7 +484,7 @@ func TestFindOrCreateGitSource_DifferentAuthCreatesNewSource(t *testing.T) { require.Len(t, sources, 2) } -func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testing.T) { +func TestSaveWorkflowGitConfig_UpdatesFileAndSourceWhenURLUnchanged(t *testing.T) { t.Parallel() _, store := datastore.MustNewTestStore(t, false, true) @@ -475,7 +494,7 @@ func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testi err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { src := &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/example/repo", TLSSkipVerify: false, Authentication: &gittypes.GitAuthentication{ @@ -489,14 +508,14 @@ func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testi sourceID = src.ID wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - StackID: 1, - ReferenceName: "refs/heads/main", - ConfigFilePath: "docker-compose.yml", - ConfigHash: "old-hash", - }, - SourceIDs: []portainer.SourceID{sourceID}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{ + SourceID: sourceID, + Path: "docker-compose.yml", + Ref: "refs/heads/main", + Hash: "old-hash", + }}, }}, } err = tx.Workflow().Create(wf) @@ -528,16 +547,16 @@ func TestSaveWorkflowGitConfig_UpdatesArtifactAndSourceWhenURLUnchanged(t *testi wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - require.Equal(t, "refs/heads/dev", wf.Artifacts[0].Artifact.ReferenceName) - require.Equal(t, "compose.yml", wf.Artifacts[0].Artifact.ConfigFilePath) - require.Equal(t, "new-hash", wf.Artifacts[0].Artifact.ConfigHash) - require.Equal(t, sourceID, wf.Artifacts[0].SourceIDs[0]) + require.Equal(t, "refs/heads/dev", wf.Artifacts[0].Files[0].Ref) + require.Equal(t, "compose.yml", wf.Artifacts[0].Files[0].Path) + require.Equal(t, "new-hash", wf.Artifacts[0].Files[0].Hash) + require.Equal(t, sourceID, wf.Artifacts[0].Files[0].SourceID) src, err := store.Source().Read(sourceID) require.NoError(t, err) - require.Equal(t, "new-user", src.GitConfig.Authentication.Username) - require.Equal(t, "new-pass", src.GitConfig.Authentication.Password) - require.True(t, src.GitConfig.TLSSkipVerify) + require.Equal(t, "new-user", src.Git.Authentication.Username) + require.Equal(t, "new-pass", src.Git.Authentication.Password) + require.True(t, src.Git.TLSSkipVerify) } func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) { @@ -549,17 +568,17 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) { err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { src := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"}, } err := tx.Source().Create(src) require.NoError(t, err) oldSourceID = src.ID wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1}, - SourceIDs: []portainer.SourceID{oldSourceID}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: oldSourceID}}, }}, } err = tx.Workflow().Create(wf) @@ -581,12 +600,12 @@ func TestSaveWorkflowGitConfig_CreatesNewSourceOnURLChange(t *testing.T) { wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - newSourceID := wf.Artifacts[0].SourceIDs[0] + newSourceID := wf.Artifacts[0].Files[0].SourceID require.NotEqual(t, oldSourceID, newSourceID) newSrc, err := store.Source().Read(newSourceID) require.NoError(t, err) - require.Equal(t, "https://github.com/example/new-repo", newSrc.GitConfig.URL) + require.Equal(t, "https://github.com/example/new-repo", newSrc.Git.URL) } func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) { @@ -598,25 +617,25 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) { err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { old := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/old-repo"}, } err := tx.Source().Create(old) require.NoError(t, err) oldSourceID = old.ID existing := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/shared-repo"}, } err = tx.Source().Create(existing) require.NoError(t, err) existingSourceID = existing.ID wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1}, - SourceIDs: []portainer.SourceID{oldSourceID}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: oldSourceID}}, }}, } err = tx.Workflow().Create(wf) @@ -638,7 +657,7 @@ func TestSaveWorkflowGitConfig_ReusesExistingSourceOnURLChange(t *testing.T) { wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - require.Equal(t, existingSourceID, wf.Artifacts[0].SourceIDs[0]) + require.Equal(t, existingSourceID, wf.Artifacts[0].Files[0].SourceID) sources, err := store.Source().ReadAll() require.NoError(t, err) @@ -659,9 +678,9 @@ func TestSaveWorkflowGitConfig_NilGitConfigReturnsError(t *testing.T) { sourceID = src.ID wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1}, - SourceIDs: []portainer.SourceID{sourceID}, + Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: sourceID}}, }}, } err = tx.Workflow().Create(wf) @@ -689,22 +708,22 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) { err := store.UpdateTx(func(tx dataservices.DataStoreTx) error { src := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, } err := tx.Source().Create(src) require.NoError(t, err) sourceID = src.ID wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{ + Artifacts: []portainer.Artifact{ { - Artifact: portainer.Artifact{StackID: 1, ConfigHash: "hash-1"}, - SourceIDs: []portainer.SourceID{sourceID}, + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "hash-1"}}, }, { - Artifact: portainer.Artifact{StackID: 2, ConfigHash: "hash-2"}, - SourceIDs: []portainer.SourceID{sourceID}, + StackID: 2, + Files: []portainer.ArtifactFile{{SourceID: sourceID, Hash: "hash-2"}}, }, }, } @@ -728,8 +747,8 @@ func TestSaveWorkflowGitConfig_OnlyMatchingArtifactUpdated(t *testing.T) { wf, err := store.Workflow().Read(workflowID) require.NoError(t, err) - require.Equal(t, "updated-hash", wf.Artifacts[0].Artifact.ConfigHash) - require.Equal(t, "hash-2", wf.Artifacts[1].Artifact.ConfigHash) + require.Equal(t, "updated-hash", wf.Artifacts[0].Files[0].Hash) + require.Equal(t, "hash-2", wf.Artifacts[1].Files[0].Hash) } func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) { @@ -741,12 +760,12 @@ func TestFindOrCreateGitSource_StripsEmbeddedCredentialsFromURL(t *testing.T) { var txErr error src, txErr = FindOrCreateGitSource(tx, &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://user:secret@github.com/example/repo", }, }) return txErr }) require.NoError(t, err) - require.Equal(t, "https://github.com/example/repo", src.GitConfig.URL) + require.Equal(t, "https://github.com/example/repo", src.Git.URL) } diff --git a/api/http/client/client.go b/api/http/client/client.go index 194131c57..68c4ecd6f 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -11,6 +11,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/rs/zerolog/log" "github.com/segmentio/encoding/json" @@ -114,18 +115,19 @@ func Get(url string, timeout int) ([]byte, error) { // using the specified host and optional TLS configuration. // It uses a new Http.Client for each operation. func ExecutePingOperation(host string, tlsConfiguration portainer.TLSConfiguration) (bool, error) { - transport := &http.Transport{} - scheme := "http" + var transport *http.Transport if tlsConfiguration.TLS { tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration) if err != nil { return false, err } - transport.TLSClientConfig = tlsConfig scheme = "https" + transport = ssrf.WrapTransport(&http.Transport{TLSClientConfig: tlsConfig}) + } else { + transport = ssrf.WrapTransport(&http.Transport{}) } client := &http.Client{ diff --git a/api/http/handler/backup/handler.go b/api/http/handler/backup/handler.go index 8a99c5ffb..de0f35af3 100644 --- a/api/http/handler/backup/handler.go +++ b/api/http/handler/backup/handler.go @@ -22,6 +22,7 @@ type Handler struct { filestorePath string shutdownTrigger context.CancelFunc adminMonitor *adminmonitor.Monitor + SetupToken string } // NewHandler creates an new instance of backup handler diff --git a/api/http/handler/backup/restore.go b/api/http/handler/backup/restore.go index 4938413ee..f8e5b7f4f 100644 --- a/api/http/handler/backup/restore.go +++ b/api/http/handler/backup/restore.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" operations "github.com/portainer/portainer/api/backup" + "github.com/portainer/portainer/api/http/security/setuptoken" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/request" ) @@ -20,15 +21,21 @@ type restorePayload struct { // @id Restore // @summary Triggers a system restore using provided backup file // @description Triggers a system restore using provided backup file -// @description **Access policy**: public +// @description **Access policy**: public (requires the X-Setup-Token header on an uninitialized instance unless --no-setup-token is set) // @tags backup // @accept json +// @param X-Setup-Token header string false "Setup token (required when instance is uninitialized and --no-setup-token is not set)" // @param restorePayload body restorePayload true "Restore request payload" // @success 200 "Success" // @failure 400 "Invalid request" +// @failure 403 "Access denied - invalid or missing setup token" // @failure 500 "Server error" // @router /restore [post] func (h *Handler) restore(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + if err := setuptoken.Validate(r, h.SetupToken); err != nil { + return err + } + initialized, err := h.adminMonitor.WasInitialized() if err != nil { return httperror.InternalServerError("Failed to check system initialization", err) diff --git a/api/http/handler/backup/restore_test.go b/api/http/handler/backup/restore_test.go index 7e5bf1d7c..1953a0cc5 100644 --- a/api/http/handler/backup/restore_test.go +++ b/api/http/handler/backup/restore_test.go @@ -14,6 +14,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/adminmonitor" "github.com/portainer/portainer/api/http/offlinegate" + "github.com/portainer/portainer/api/http/security/setuptoken" "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" @@ -126,6 +127,45 @@ func backup(t *testing.T, h *Handler, password string) []byte { return archive } +func Test_restore_setupTokenGate(t *testing.T) { + t.Parallel() + datastore := testhelpers.NewDatastore( + testhelpers.WithUsers([]portainer.User{}), + testhelpers.WithEdgeJobs([]portainer.EdgeJob{}), + ) + adminMonitor := adminmonitor.New(time.Hour, datastore) + h := NewHandler( + testhelpers.NewTestRequestBouncer(), + datastore, + offlinegate.NewOfflineGate(), + prepareFilestorePath(t), + func() {}, + adminMonitor, + ) + h.SetupToken = "secret-token" + + t.Run("403 without token header", func(t *testing.T) { + err := h.restore(httptest.NewRecorder(), prepareMultipartRequest(t, "", []byte("x"))) + require.Error(t, err) + assert.Equal(t, http.StatusForbidden, err.StatusCode) + }) + + t.Run("403 with wrong token", func(t *testing.T) { + r := prepareMultipartRequest(t, "", []byte("x")) + r.Header.Set(setuptoken.HeaderName, "wrong") + err := h.restore(httptest.NewRecorder(), r) + require.Error(t, err) + assert.Equal(t, http.StatusForbidden, err.StatusCode) + }) + + t.Run("passes gate with correct token", func(t *testing.T) { + archive := backup(t, h, "") + r := prepareMultipartRequest(t, "", archive) + r.Header.Set(setuptoken.HeaderName, "secret-token") + require.Nil(t, h.restore(httptest.NewRecorder(), r)) + }) +} + func prepareMultipartRequest(t *testing.T, password string, file []byte) *http.Request { var body bytes.Buffer diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go index 88ad28b93..826dfe0b0 100644 --- a/api/http/handler/customtemplates/customtemplate_create.go +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -5,12 +5,13 @@ import ( "errors" "net/http" "os" - "regexp" "strconv" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/filesystem" gittypes "github.com/portainer/portainer/api/git/types" + "github.com/portainer/portainer/api/gitops/workflows" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/stacks/stackutils" @@ -41,30 +42,39 @@ func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Requ customTemplate.CreatedByUserID = tokenData.ID - customTemplates, err := handler.DataStore.CustomTemplate().ReadAll() + err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error { + return createCustomTemplateTx(tx, customTemplate, tokenData.ID) + }) + + return response.TxResponse(w, customTemplate, err) +} + +func createCustomTemplateTx(tx dataservices.DataStoreTx, customTemplate *portainer.CustomTemplate, userID portainer.UserID) error { + existingTemplates, err := tx.CustomTemplate().ReadAll() if err != nil { return httperror.InternalServerError("Unable to retrieve custom templates from the database", err) } - for _, existingTemplate := range customTemplates { - if existingTemplate.Title == customTemplate.Title { + for _, existing := range existingTemplates { + if existing.Title == customTemplate.Title { return httperror.InternalServerError("Template name must be unique", errors.New("Template name must be unique")) } } - if err := handler.DataStore.CustomTemplate().Create(customTemplate); err != nil { + if err := tx.CustomTemplate().Create(customTemplate); err != nil { return httperror.InternalServerError("Unable to create custom template", err) } - resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, tokenData.ID) + resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, userID) - if err := handler.DataStore.ResourceControl().Create(resourceControl); err != nil { + if err := tx.ResourceControl().Create(resourceControl); err != nil { return httperror.InternalServerError("Unable to persist resource control inside the database", err) } customTemplate.ResourceControl = resourceControl + populateGitConfig(tx, customTemplate) - return response.JSON(w, customTemplate) + return nil } func (handler *Handler) createCustomTemplate(method string, r *http.Request) (*portainer.CustomTemplate, error) { @@ -122,19 +132,11 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack { return errors.New("Invalid custom template type") } - if !isValidNote(payload.Note) { + if !IsValidNote(payload.Note) { return errors.New("Invalid note. tag is not supported") } - return validateVariablesDefinitions(payload.Variables) -} - -func isValidNote(note string) bool { - if len(note) == 0 { - return true - } - match, _ := regexp.MatchString(" tag is not supported") } - return validateVariablesDefinitions(payload.Variables) + return ValidateVariablesDefinitions(payload.Variables) } // @id CustomTemplateCreateRepository @@ -312,9 +314,27 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) ( return nil, err } - gitConfig.ConfigHash = commitHash - customTemplate.GitConfig = gitConfig + src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{ + Name: gittypes.RepoName(gitConfig.URL), + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{ + URL: gitConfig.URL, + Authentication: gitConfig.Authentication, + TLSSkipVerify: gitConfig.TLSSkipVerify, + }, + }) + if err != nil { + return nil, err + } + customTemplate.Artifact = &portainer.Artifact{ + Files: []portainer.ArtifactFile{{ + SourceID: src.ID, + Path: gitConfig.ConfigFilePath, + Ref: gitConfig.ReferenceName, + Hash: commitHash, + }}, + } isValidProject := true defer func() { if !isValidProject { @@ -390,7 +410,7 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er payload.Logo = logo note, _ := request.RetrieveMultiPartFormValue(r, "Note", true) - if !isValidNote(note) { + if !IsValidNote(note) { return errors.New("Invalid note. tag is not supported") } payload.Note = note @@ -422,7 +442,7 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er if err := json.Unmarshal([]byte(varsString), &payload.Variables); err != nil { return errors.New("Invalid variables. Ensure that the variables are valid JSON") } - if err := validateVariablesDefinitions(payload.Variables); err != nil { + if err := ValidateVariablesDefinitions(payload.Variables); err != nil { return err } } diff --git a/api/http/handler/customtemplates/customtemplate_create_test.go b/api/http/handler/customtemplates/customtemplate_create_test.go new file mode 100644 index 000000000..30d909213 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_create_test.go @@ -0,0 +1,1037 @@ +package customtemplates + +import ( + "bytes" + "context" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/security" + + "github.com/gorilla/mux" + "github.com/segmentio/encoding/json" + "github.com/stretchr/testify/require" +) + +func createTemplateRequest(t *testing.T, method string, payload any, userID portainer.UserID, role portainer.UserRole) *http.Request { + t.Helper() + + body, err := json.Marshal(payload) + require.NoError(t, err) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/"+method, bytes.NewReader(body)) + r.Header.Set("Content-Type", "application/json") + r = mux.SetURLVars(r, map[string]string{"method": method}) + + return r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: userID, Role: role})) +} + +func TestCustomTemplateCreate_FromFileContent_Success(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + FileContent: "version: '3'\nservices:\n web:\n image: nginx", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "My Template", tmpl.Title) + require.Equal(t, "A test template", tmpl.Description) + require.Equal(t, portainer.UserID(1), tmpl.CreatedByUserID) + require.NotNil(t, tmpl.ResourceControl) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + templates, err := tx.CustomTemplate().ReadAll() + require.NoError(t, err) + require.Len(t, templates, 1) + require.Equal(t, "My Template", templates[0].Title) + + rcs, err := tx.ResourceControl().ReadAll() + require.NoError(t, err) + require.Len(t, rcs, 1) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromFileContent_DuplicateTitle(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "Existing Template", + }) + })) + + payload := customTemplateFromFileContentPayload{ + Title: "Existing Template", + Description: "Another template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_MissingTitle(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Description: "A test template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_MissingDescription(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_MissingFileContent(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_InvalidPlatform(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: 0, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_NoteWithImage(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + FileContent: "version: '3'", + Note: `Some note with `, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_KubernetesTypeIgnoresPlatform(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "K8s Template", + Description: "A kubernetes template", + FileContent: "apiVersion: v1\nkind: Pod", + Type: portainer.KubernetesStack, + Platform: 0, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "K8s Template", tmpl.Title) + require.Equal(t, portainer.KubernetesStack, tmpl.Type) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + templates, err := tx.CustomTemplate().ReadAll() + require.NoError(t, err) + require.Len(t, templates, 1) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromFileUpload_Success(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "Uploaded Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "Uploaded from file") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'\nservices:\n web:\n image: nginx")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "Uploaded Template", tmpl.Title) + require.Equal(t, filesystem.ComposeFileDefaultName, tmpl.EntryPoint) + + err = ds.ViewTx(func(tx dataservices.DataStoreTx) error { + templates, err := tx.CustomTemplate().ReadAll() + require.NoError(t, err) + require.Len(t, templates, 1) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_InvalidMethod(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "invalid", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_InvalidType(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + FileContent: "version: '3'", + Type: 0, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_Variables(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "Template With Variables", + Description: "A test template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + Variables: []portainer.CustomTemplateVariableDefinition{ + {Name: "IMAGE", Label: "Docker image"}, + }, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Len(t, tmpl.Variables, 1) + require.Equal(t, "IMAGE", tmpl.Variables[0].Name) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(tmpl.ID) + require.NoError(t, err) + require.Len(t, stored.Variables, 1) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromFileContent_InvalidVariables(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "My Template", + Description: "A test template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + Variables: []portainer.CustomTemplateVariableDefinition{ + {Label: "Missing name"}, + }, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileContent_EdgeTemplate(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + payload := customTemplateFromFileContentPayload{ + Title: "Edge Template", + Description: "For edge stacks", + FileContent: "version: '3'\nservices:\n web:\n image: nginx", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + EdgeTemplate: true, + } + + r := createTemplateRequest(t, "string", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.True(t, tmpl.EdgeTemplate) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(tmpl.ID) + require.NoError(t, err) + require.True(t, stored.EdgeTemplate) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromFileUpload_MissingTitle(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileUpload_MissingDescription(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "My Template") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileUpload_MissingFile(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "My Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileUpload_InvalidType(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "My Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Type", "0") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileUpload_InvalidPlatform(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "My Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "0") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileUpload_NoteWithImage(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "My Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Note", `Some note `) + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromFileUpload_KubernetesIgnoresPlatform(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "K8s Upload Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "A kubernetes template") + require.NoError(t, err) + + err = writer.WriteField("Type", "3") + require.NoError(t, err) + + err = writer.WriteField("Platform", "0") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "deployment.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("apiVersion: v1\nkind: Pod")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "K8s Upload Template", tmpl.Title) + require.Equal(t, filesystem.ComposeFileDefaultName, tmpl.EntryPoint) + + err = ds.ViewTx(func(tx dataservices.DataStoreTx) error { + templates, err := tx.CustomTemplate().ReadAll() + require.NoError(t, err) + require.Len(t, templates, 1) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromFileUpload_Variables(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + vars, err := json.Marshal([]portainer.CustomTemplateVariableDefinition{{Name: "IMAGE", Label: "Docker image"}}) + require.NoError(t, err) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err = writer.WriteField("Title", "Template With Variables") + require.NoError(t, err) + + err = writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + err = writer.WriteField("Variables", string(vars)) + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Len(t, tmpl.Variables, 1) + require.Equal(t, "IMAGE", tmpl.Variables[0].Name) + + err = ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(tmpl.ID) + require.NoError(t, err) + require.Len(t, stored.Variables, 1) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromFileUpload_InvalidVariables(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + err := writer.WriteField("Title", "My Template") + require.NoError(t, err) + + err = writer.WriteField("Description", "A description") + require.NoError(t, err) + + err = writer.WriteField("Type", "2") + require.NoError(t, err) + + err = writer.WriteField("Platform", "1") + require.NoError(t, err) + + err = writer.WriteField("Variables", "not-valid-json") + require.NoError(t, err) + + part, err := writer.CreateFormFile("File", "docker-compose.yml") + require.NoError(t, err) + + _, err = part.Write([]byte("version: '3'")) + require.NoError(t, err) + + require.NoError(t, writer.Close()) + + r := httptest.NewRequest(http.MethodPost, "/custom_templates/create/file", &body) + r.Header.Set("Content-Type", writer.FormDataContentType()) + r = mux.SetURLVars(r, map[string]string{"method": "file"}) + r = r.WithContext(security.StoreTokenData(r, &portainer.TokenData{ID: 1, Role: portainer.AdministratorRole})) + + rr := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +type gitServiceCreatingFile struct { + portainer.GitService +} + +func (g *gitServiceCreatingFile) CloneRepository(_ context.Context, destination, _, _, _, _ string, _ bool) error { + if err := os.MkdirAll(destination, 0700); err != nil { + return err + } + + f, err := os.Create(filesystem.JoinPaths(destination, filesystem.ComposeFileDefaultName)) + if err != nil { + return err + } + + return f.Close() +} + +func (g *gitServiceCreatingFile) LatestCommitID(_ context.Context, _, _, _, _ string, _ bool) (string, error) { + return "deadbeef123", nil +} + +func TestCustomTemplateCreate_FromRepository_Success(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + handler.GitService = &gitServiceCreatingFile{} + + payload := customTemplateFromGitRepositoryPayload{ + Title: "Git Template", + Description: "Created from git", + RepositoryURL: "https://github.com/example/repo", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "Git Template", tmpl.Title) + require.NotNil(t, tmpl.Artifact) + require.Len(t, tmpl.Artifact.Files, 1) + require.Equal(t, "deadbeef123", tmpl.Artifact.Files[0].Hash) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(tmpl.ID) + require.NoError(t, err) + require.NotNil(t, stored.Artifact) + + src, err := tx.Source().Read(stored.Artifact.Files[0].SourceID) + require.NoError(t, err) + require.Equal(t, portainer.SourceTypeGit, src.Type) + require.Equal(t, "https://github.com/example/repo", src.Git.URL) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromRepository_DeduplicatesSource(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + handler.GitService = &gitServiceCreatingFile{} + + makePayload := func(title string) customTemplateFromGitRepositoryPayload { + return customTemplateFromGitRepositoryPayload{ + Title: title, + Description: "Created from git", + RepositoryURL: "https://github.com/example/repo", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + } + + r1 := createTemplateRequest(t, "repository", makePayload("Template One"), 1, portainer.AdministratorRole) + rr1 := httptest.NewRecorder() + herr := handler.customTemplateCreate(rr1, r1) + require.Nil(t, herr) + + r2 := createTemplateRequest(t, "repository", makePayload("Template Two"), 1, portainer.AdministratorRole) + rr2 := httptest.NewRecorder() + herr = handler.customTemplateCreate(rr2, r2) + require.Nil(t, herr) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + sources, err := tx.Source().ReadAll() + require.NoError(t, err) + require.Len(t, sources, 1, "two templates with the same URL must share one Source") + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateCreate_FromRepository_Validation_MissingTitle(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromGitRepositoryPayload{ + Description: "A description", + RepositoryURL: "https://github.com/example/repo", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromRepository_Validation_MissingDescription(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromGitRepositoryPayload{ + Title: "My Template", + RepositoryURL: "https://github.com/example/repo", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromRepository_Validation_InvalidURL(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromGitRepositoryPayload{ + Title: "My Template", + Description: "A description", + RepositoryURL: "http://", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromRepository_Validation_AuthWithoutCredentials(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromGitRepositoryPayload{ + Title: "My Template", + Description: "A description", + RepositoryURL: "https://github.com/example/repo", + RepositoryAuthentication: true, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromRepository_Validation_InvalidPlatform(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromGitRepositoryPayload{ + Title: "My Template", + Description: "A description", + RepositoryURL: "https://github.com/example/repo", + Type: portainer.DockerComposeStack, + Platform: 0, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateCreate_FromRepository_Validation_InvalidType(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateFromGitRepositoryPayload{ + Title: "My Template", + Description: "A description", + RepositoryURL: "https://github.com/example/repo", + Type: 0, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := createTemplateRequest(t, "repository", payload, 1, portainer.AdministratorRole) + rr := httptest.NewRecorder() + + herr := handler.customTemplateCreate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} diff --git a/api/http/handler/customtemplates/customtemplate_file.go b/api/http/handler/customtemplates/customtemplate_file.go index 286da475d..463a953a5 100644 --- a/api/http/handler/customtemplates/customtemplate_file.go +++ b/api/http/handler/customtemplates/customtemplate_file.go @@ -82,8 +82,8 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques } entryPath := customTemplate.EntryPoint - if customTemplate.GitConfig != nil { - entryPath = customTemplate.GitConfig.ConfigFilePath + if customTemplate.Artifact != nil && len(customTemplate.Artifact.Files) > 0 { + entryPath = customTemplate.Artifact.Files[0].Path } fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, entryPath) if err != nil { diff --git a/api/http/handler/customtemplates/customtemplate_file_test.go b/api/http/handler/customtemplates/customtemplate_file_test.go index cc1f7fa50..d2d818dcd 100644 --- a/api/http/handler/customtemplates/customtemplate_file_test.go +++ b/api/http/handler/customtemplates/customtemplate_file_test.go @@ -5,11 +5,13 @@ import ( "net/http/httptest" "testing" - "github.com/gorilla/mux" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" + gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/http/security" httperror "github.com/portainer/portainer/pkg/libhttp/error" + + "github.com/gorilla/mux" "github.com/segmentio/encoding/json" "github.com/stretchr/testify/require" ) @@ -33,7 +35,8 @@ func TestCustomTemplateFile(t *testing.T) { require.NoError(t, err) require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ID: 2, EntryPoint: templateEntrypoint, ProjectPath: path})) - require.NoError(t, tx.ResourceControl().Create(&portainer.ResourceControl{ID: 1, ResourceID: "2", Type: portainer.CustomTemplateResourceControl, + require.NoError(t, tx.ResourceControl().Create(&portainer.ResourceControl{ + ID: 1, ResourceID: "2", Type: portainer.CustomTemplateResourceControl, UserAccesses: []portainer.UserResourceAccess{{UserID: 2}}, TeamAccesses: []portainer.TeamResourceAccess{{TeamID: 1}}, })) @@ -59,6 +62,7 @@ func TestCustomTemplateFile(t *testing.T) { rr, r := test("1", &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) require.Nil(t, r) require.Equal(t, http.StatusOK, rr.Result().StatusCode) + var res struct{ FileContent string } require.NoError(t, json.NewDecoder(rr.Body).Decode(&res)) require.Equal(t, templateContent, res.FileContent) @@ -74,6 +78,7 @@ func TestCustomTemplateFile(t *testing.T) { rr, r := test("2", &security.RestrictedRequestContext{UserID: 2}) require.Nil(t, r) require.Equal(t, http.StatusOK, rr.Result().StatusCode) + var res struct{ FileContent string } require.NoError(t, json.NewDecoder(rr.Body).Decode(&res)) require.Equal(t, templateContent, res.FileContent) @@ -83,6 +88,7 @@ func TestCustomTemplateFile(t *testing.T) { rr, r := test("2", &security.RestrictedRequestContext{UserID: 3, UserMemberships: []portainer.TeamMembership{{ID: 1, UserID: 3, TeamID: 1}}}) require.Nil(t, r) require.Equal(t, http.StatusOK, rr.Result().StatusCode) + var res struct{ FileContent string } require.NoError(t, json.NewDecoder(rr.Body).Decode(&res)) require.Equal(t, templateContent, res.FileContent) @@ -94,3 +100,46 @@ func TestCustomTemplateFile(t *testing.T) { require.Equal(t, http.StatusForbidden, r.StatusCode) }) } + +func TestCustomTemplateFile_GitTemplate(t *testing.T) { + t.Parallel() + + handler, ds, fs := newTestHandler(t) + + templateContent := "git template content" + configFilePath := "docker-compose.yml" + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{ + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/example/repo"}, + } + err := tx.Source().Create(src) + require.NoError(t, err) + + path, err := fs.StoreCustomTemplateFileFromBytes("10", configFilePath, []byte(templateContent)) + require.NoError(t, err) + + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 10, + EntryPoint: "should-not-be-used.yml", + ProjectPath: path, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{Path: configFilePath, SourceID: src.ID}}, + }, + }) + })) + + r := httptest.NewRequest(http.MethodGet, "/custom_templates/10/file", nil) + r = mux.SetURLVars(r, map[string]string{"id": "10"}) + ctx := security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + r = r.WithContext(ctx) + rr := httptest.NewRecorder() + herr := handler.customTemplateFile(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Result().StatusCode) + + var res struct{ FileContent string } + require.NoError(t, json.NewDecoder(rr.Body).Decode(&res)) + require.Equal(t, templateContent, res.FileContent) +} diff --git a/api/http/handler/customtemplates/customtemplate_git_fetch.go b/api/http/handler/customtemplates/customtemplate_git_fetch.go index ec7f25376..6230ab812 100644 --- a/api/http/handler/customtemplates/customtemplate_git_fetch.go +++ b/api/http/handler/customtemplates/customtemplate_git_fetch.go @@ -7,6 +7,7 @@ import ( "sync" portainer "github.com/portainer/portainer/api" + gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/stacks/stackutils" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/request" @@ -42,8 +43,28 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re return httperror.InternalServerError("Unable to find a custom template with the specified identifier inside the database", err) } - if customTemplate.GitConfig == nil { - return httperror.BadRequest("Git configuration does not exist in this custom template", err) + if customTemplate.Artifact == nil || len(customTemplate.Artifact.Files) == 0 { + return httperror.BadRequest("Git configuration does not exist in this custom template", nil) + } + + file := customTemplate.Artifact.Files[0] + + src, err := handler.DataStore.Source().Read(file.SourceID) + if err != nil { + return httperror.InternalServerError("Unable to retrieve git source for custom template", err) + } + + if src.Git == nil { + return httperror.InternalServerError("Source has no git configuration", nil) + } + + gitConfig := &gittypes.RepoConfig{ + URL: src.Git.URL, + Authentication: src.Git.Authentication, + TLSSkipVerify: src.Git.TLSSkipVerify, + ReferenceName: file.Ref, + ConfigFilePath: file.Path, + ConfigHash: file.Hash, } // If multiple users are trying to fetch the same custom template simultaneously, a lock needs to be added @@ -68,7 +89,7 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re } }() - commitHash, err := stackutils.DownloadGitRepository(context.TODO(), *customTemplate.GitConfig, handler.GitService, func() string { + commitHash, err := stackutils.DownloadGitRepository(context.TODO(), *gitConfig, handler.GitService, func() string { return customTemplate.ProjectPath }) if err != nil { @@ -81,15 +102,15 @@ func (handler *Handler) customTemplateGitFetch(w http.ResponseWriter, r *http.Re return httperror.InternalServerError("Failed to download git repository", err) } - if customTemplate.GitConfig.ConfigHash != commitHash { - customTemplate.GitConfig.ConfigHash = commitHash + if customTemplate.Artifact.Files[0].Hash != commitHash { + customTemplate.Artifact.Files[0].Hash = commitHash if err := handler.DataStore.CustomTemplate().Update(customTemplate.ID, customTemplate); err != nil { return httperror.InternalServerError("Unable to persist custom template changes inside the database", err) } } - fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.GitConfig.ConfigFilePath) + fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, gitConfig.ConfigFilePath) if err != nil { return httperror.InternalServerError("Unable to retrieve custom template file from disk", err) } diff --git a/api/http/handler/customtemplates/customtemplate_git_fetch_test.go b/api/http/handler/customtemplates/customtemplate_git_fetch_test.go index 7f72cca8a..9a94aca66 100644 --- a/api/http/handler/customtemplates/customtemplate_git_fetch_test.go +++ b/api/http/handler/customtemplates/customtemplate_git_fetch_test.go @@ -156,6 +156,7 @@ func singleAPIRequest(h *Handler, jwt string, expect string) error { func Test_customTemplateGitFetch(t *testing.T) { t.Parallel() + is := assert.New(t) _, store := datastore.MustNewTestStore(t, true, true) @@ -172,12 +173,31 @@ func Test_customTemplateGitFetch(t *testing.T) { dir, err := os.Getwd() require.NoError(t, err, "error to get working directory") - template1 := &portainer.CustomTemplate{ID: 1, Title: "custom-template-1", ProjectPath: filesystem.JoinPaths(dir, "fixtures/custom_template_1"), GitConfig: &gittypes.RepoConfig{ConfigFilePath: "test-config-path.txt"}} + src := &portainer.Source{ + ID: 1, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + }, + } + err = store.Source().Create(src) + require.NoError(t, err, "error creating source") + + const configFilePath = "test-config-path.txt" + + template1 := &portainer.CustomTemplate{ + ID: 1, + Title: "custom-template-1", + ProjectPath: filesystem.JoinPaths(dir, "fixtures/custom_template_1"), + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{Path: configFilePath, SourceID: src.ID}}, + }, + } err = store.CustomTemplateService.Create(template1) require.NoError(t, err, "error creating custom template 1") // prepare testing folder - err = prepareTestFolder(template1.ProjectPath, template1.GitConfig.ConfigFilePath) + err = prepareTestFolder(template1.ProjectPath, configFilePath) require.NoError(t, err, "error creating testing folder") defer func() { @@ -192,7 +212,7 @@ func Test_customTemplateGitFetch(t *testing.T) { requestBouncer := security.NewRequestBouncer(t.Context(), store, jwtService, nil) gitService := &TestGitService{ - targetFilePath: filesystem.JoinPaths(template1.ProjectPath, template1.GitConfig.ConfigFilePath), + targetFilePath: filesystem.JoinPaths(template1.ProjectPath, configFilePath), } fileService := &TestFileService{} @@ -252,7 +272,7 @@ func Test_customTemplateGitFetch(t *testing.T) { t.Run("restore git repository if it is failed to download the new git repository", func(t *testing.T) { invalidGitService := &InvalidTestGitService{ - targetFilePath: filesystem.JoinPaths(template1.ProjectPath, template1.GitConfig.ConfigFilePath), + targetFilePath: filesystem.JoinPaths(template1.ProjectPath, configFilePath), } h := NewHandler(requestBouncer, store, fileService, invalidGitService) @@ -274,3 +294,73 @@ func Test_customTemplateGitFetch(t *testing.T) { assert.Equal(t, "gfedcba", string(fileContent)) }) } + +func TestCustomTemplateGitFetch_NilArtifactReturnsBadRequest(t *testing.T) { + t.Parallel() + + _, store := datastore.MustNewTestStore(t, false, true) + + template := &portainer.CustomTemplate{ID: 1, Title: "no-git-template"} + err := store.CustomTemplateService.Create(template) + require.NoError(t, err) + + h := NewHandler(testhelpers.NewTestRequestBouncer(), store, &TestFileService{}, &TestGitService{}) + + req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBufferString("{}")) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + require.Equal(t, http.StatusBadRequest, rr.Code) +} + +func TestCustomTemplateGitFetch_EmptySourceIDsReturnsBadRequest(t *testing.T) { + t.Parallel() + + _, store := datastore.MustNewTestStore(t, false, true) + + template := &portainer.CustomTemplate{ + ID: 1, + Title: "empty-source-ids", + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{}, + }, + } + err := store.CustomTemplateService.Create(template) + require.NoError(t, err) + + h := NewHandler(testhelpers.NewTestRequestBouncer(), store, &TestFileService{}, &TestGitService{}) + + req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBufferString("{}")) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + require.Equal(t, http.StatusBadRequest, rr.Code) +} + +func TestCustomTemplateGitFetch_SourceWithNilGitConfigReturnsInternalError(t *testing.T) { + t.Parallel() + + _, store := datastore.MustNewTestStore(t, false, true) + + src := &portainer.Source{Type: portainer.SourceTypeGit} + err := store.Source().Create(src) + require.NoError(t, err) + + template := &portainer.CustomTemplate{ + ID: 1, + Title: "nil-git-config", + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{SourceID: src.ID}}, + }, + } + err = store.CustomTemplateService.Create(template) + require.NoError(t, err) + + h := NewHandler(testhelpers.NewTestRequestBouncer(), store, &TestFileService{}, &TestGitService{}) + + req := httptest.NewRequest(http.MethodPut, "/custom_templates/1/git_fetch", bytes.NewBufferString("{}")) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, req) + + require.Equal(t, http.StatusInternalServerError, rr.Code) +} diff --git a/api/http/handler/customtemplates/customtemplate_inspect.go b/api/http/handler/customtemplates/customtemplate_inspect.go index 716bc8395..018a492a0 100644 --- a/api/http/handler/customtemplates/customtemplate_inspect.go +++ b/api/http/handler/customtemplates/customtemplate_inspect.go @@ -68,11 +68,13 @@ func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Req } - if canEdit || hasAccess { - return nil + if !canEdit && !hasAccess { + return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied) } - return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied) + populateGitConfig(tx, customTemplate) + + return nil }) return response.TxResponse(w, customTemplate, err) diff --git a/api/http/handler/customtemplates/customtemplate_inspect_test.go b/api/http/handler/customtemplates/customtemplate_inspect_test.go index 40ec7b916..ef8a90739 100644 --- a/api/http/handler/customtemplates/customtemplate_inspect_test.go +++ b/api/http/handler/customtemplates/customtemplate_inspect_test.go @@ -5,14 +5,16 @@ import ( "net/http/httptest" "testing" - "github.com/gorilla/mux" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/filesystem" + gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/testhelpers" httperror "github.com/portainer/portainer/pkg/libhttp/error" + + "github.com/gorilla/mux" "github.com/segmentio/encoding/json" "github.com/stretchr/testify/require" ) @@ -33,11 +35,13 @@ func newTestHandler(t *testing.T) (*Handler, dataservices.DataStore, portainer.F require.NoError(t, tx.User().Create(&portainer.User{ID: 2, Username: "std2", Role: portainer.StandardUserRole})) require.NoError(t, tx.User().Create(&portainer.User{ID: 3, Username: "std3", Role: portainer.StandardUserRole})) require.NoError(t, tx.User().Create(&portainer.User{ID: 4, Username: "std4", Role: portainer.StandardUserRole})) - require.NoError(t, tx.Endpoint().Create(&portainer.Endpoint{ID: 1, + require.NoError(t, tx.Endpoint().Create(&portainer.Endpoint{ + ID: 1, UserAccessPolicies: portainer.UserAccessPolicies{ 2: portainer.AccessPolicy{RoleID: 0}, 3: portainer.AccessPolicy{RoleID: 0}, - }})) + }, + })) require.NoError(t, tx.Team().Create(&portainer.Team{ID: 1})) require.NoError(t, tx.TeamMembership().Create(&portainer.TeamMembership{ID: 1, UserID: 3, TeamID: 1, Role: portainer.TeamMember})) return nil @@ -56,7 +60,8 @@ func TestInspectHandler(t *testing.T) { require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ID: 1})) require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ID: 2})) - require.NoError(t, tx.ResourceControl().Create(&portainer.ResourceControl{ID: 1, ResourceID: "2", Type: portainer.CustomTemplateResourceControl, + require.NoError(t, tx.ResourceControl().Create(&portainer.ResourceControl{ + ID: 1, ResourceID: "2", Type: portainer.CustomTemplateResourceControl, UserAccesses: []portainer.UserResourceAccess{{UserID: 2}}, TeamAccesses: []portainer.TeamResourceAccess{{TeamID: 1}}, })) @@ -82,6 +87,7 @@ func TestInspectHandler(t *testing.T) { rr, r := test("1", &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) require.Nil(t, r) require.Equal(t, http.StatusOK, rr.Result().StatusCode) + var template portainer.CustomTemplate require.NoError(t, json.NewDecoder(rr.Body).Decode(&template)) require.Equal(t, portainer.CustomTemplateID(1), template.ID) @@ -97,6 +103,7 @@ func TestInspectHandler(t *testing.T) { rr, r := test("2", &security.RestrictedRequestContext{UserID: 2}) require.Nil(t, r) require.Equal(t, http.StatusOK, rr.Result().StatusCode) + var template portainer.CustomTemplate require.NoError(t, json.NewDecoder(rr.Body).Decode(&template)) require.Equal(t, portainer.CustomTemplateID(2), template.ID) @@ -106,6 +113,7 @@ func TestInspectHandler(t *testing.T) { rr, r := test("2", &security.RestrictedRequestContext{UserID: 3, UserMemberships: []portainer.TeamMembership{{ID: 1, UserID: 3, TeamID: 1}}}) require.Nil(t, r) require.Equal(t, http.StatusOK, rr.Result().StatusCode) + var template portainer.CustomTemplate require.NoError(t, json.NewDecoder(rr.Body).Decode(&template)) require.Equal(t, portainer.CustomTemplateID(2), template.ID) @@ -117,3 +125,53 @@ func TestInspectHandler(t *testing.T) { require.Equal(t, http.StatusForbidden, r.StatusCode) }) } + +func TestInspectHandler_GitConfigPopulatedFromSource(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + var srcID portainer.SourceID + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{ + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + TLSSkipVerify: true, + }, + } + err := tx.Source().Create(src) + require.NoError(t, err) + srcID = src.ID + + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 10, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{ + Ref: "refs/heads/main", + Path: "docker-compose.yml", + Hash: "abc123", + SourceID: srcID, + }}, + }, + }) + })) + + r := httptest.NewRequest(http.MethodGet, "/custom_templates/10", nil) + r = mux.SetURLVars(r, map[string]string{"id": "10"}) + ctx := security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + r = r.WithContext(ctx) + rr := httptest.NewRecorder() + herr := handler.customTemplateInspect(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Result().StatusCode) + + var template portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&template)) + require.NotNil(t, template.GitConfig) + require.Equal(t, "https://github.com/example/repo", template.GitConfig.URL) + require.True(t, template.GitConfig.TLSSkipVerify) + require.Equal(t, "refs/heads/main", template.GitConfig.ReferenceName) + require.Equal(t, "docker-compose.yml", template.GitConfig.ConfigFilePath) + require.Equal(t, "abc123", template.GitConfig.ConfigHash) +} diff --git a/api/http/handler/customtemplates/customtemplate_list.go b/api/http/handler/customtemplates/customtemplate_list.go index 4c69cbb89..36bcbff9d 100644 --- a/api/http/handler/customtemplates/customtemplate_list.go +++ b/api/http/handler/customtemplates/customtemplate_list.go @@ -74,10 +74,7 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques } for i := range customTemplates { - customTemplate := &customTemplates[i] - if customTemplate.GitConfig != nil && customTemplate.GitConfig.Authentication != nil { - customTemplate.GitConfig.Authentication.Password = "" - } + populateGitConfig(handler.DataStore, &customTemplates[i]) } return response.JSON(w, customTemplates) diff --git a/api/http/handler/customtemplates/customtemplate_list_test.go b/api/http/handler/customtemplates/customtemplate_list_test.go new file mode 100644 index 000000000..6cfaf617c --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_list_test.go @@ -0,0 +1,127 @@ +package customtemplates + +import ( + "net/http" + "net/http/httptest" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + gittypes "github.com/portainer/portainer/api/git/types" + "github.com/portainer/portainer/api/http/security" + + "github.com/segmentio/encoding/json" + "github.com/stretchr/testify/require" +) + +func TestCustomTemplateList_PopulatesGitConfigFromSource(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + var srcID portainer.SourceID + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{ + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + TLSSkipVerify: true, + }, + } + err := tx.Source().Create(src) + require.NoError(t, err) + srcID = src.ID + require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{ + Ref: "refs/heads/main", + Path: "docker-compose.yml", + Hash: "abc123", + SourceID: srcID, + }}, + }, + })) + require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ID: 2, EntryPoint: "docker-compose.yml"})) + + return nil + })) + + r := httptest.NewRequest(http.MethodGet, "/custom_templates", nil) + r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, r) + + require.Equal(t, http.StatusOK, rr.Code) + + var templates []portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&templates)) + + var gitTemplate portainer.CustomTemplate + for _, tpl := range templates { + if tpl.ID == 1 { + gitTemplate = tpl + } + } + + require.NotNil(t, gitTemplate.GitConfig) + require.Equal(t, "https://github.com/example/repo", gitTemplate.GitConfig.URL) + require.True(t, gitTemplate.GitConfig.TLSSkipVerify) + require.Equal(t, "refs/heads/main", gitTemplate.GitConfig.ReferenceName) + require.Equal(t, "docker-compose.yml", gitTemplate.GitConfig.ConfigFilePath) + require.Equal(t, "abc123", gitTemplate.GitConfig.ConfigHash) + + var plainTemplate portainer.CustomTemplate + for _, tpl := range templates { + if tpl.ID == 2 { + plainTemplate = tpl + } + } + require.Nil(t, plainTemplate.GitConfig) +} + +func TestCustomTemplateList_StripsPasswordFromGitConfig(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + var srcID portainer.SourceID + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{ + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{ + URL: "https://github.com/example/repo", + Authentication: &gittypes.GitAuthentication{ + Username: "user", + Password: "topsecret", + }, + }, + } + err := tx.Source().Create(src) + require.NoError(t, err) + srcID = src.ID + require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{SourceID: srcID}}, + }, + })) + + return nil + })) + + r := httptest.NewRequest(http.MethodGet, "/custom_templates", nil) + r = r.WithContext(security.StoreRestrictedRequestContext(r, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, r) + + require.Equal(t, http.StatusOK, rr.Code) + + var templates []portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&templates)) + require.Len(t, templates, 1) + require.NotNil(t, templates[0].GitConfig) + require.NotNil(t, templates[0].GitConfig.Authentication) + require.Equal(t, "user", templates[0].GitConfig.Authentication.Username) + require.Empty(t, templates[0].GitConfig.Authentication.Password) +} diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go index 794d06a6c..7aeea8e25 100644 --- a/api/http/handler/customtemplates/customtemplate_update.go +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -8,9 +8,11 @@ import ( "strconv" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/git" gittypes "github.com/portainer/portainer/api/git/types" + "github.com/portainer/portainer/api/gitops/workflows" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" httperror "github.com/portainer/portainer/pkg/libhttp/error" @@ -84,7 +86,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { return errors.New("Invalid custom template description") } - if !isValidNote(payload.Note) { + if !IsValidNote(payload.Note) { return errors.New("Invalid note. tag is not supported") } @@ -96,7 +98,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } - if err := validateVariablesDefinitions(payload.Variables); err != nil { + if err := ValidateVariablesDefinitions(payload.Variables); err != nil { return err } @@ -218,8 +220,28 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ return httperror.InternalServerError("Unable get latest commit id", fmt.Errorf("failed to fetch latest commit id of the template %v: %w", customTemplate.ID, err)) } - gitConfig.ConfigHash = commitHash - customTemplate.GitConfig = gitConfig + src, err := workflows.FindOrCreateGitSource(handler.DataStore, &portainer.Source{ + Name: gittypes.RepoName(gitConfig.URL), + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{ + URL: gitConfig.URL, + Authentication: gitConfig.Authentication, + TLSSkipVerify: gitConfig.TLSSkipVerify, + }, + }) + if err != nil { + return httperror.InternalServerError("Unable to find or create git source", err) + } + + customTemplate.Artifact = &portainer.Artifact{ + Files: []portainer.ArtifactFile{{ + SourceID: src.ID, + Path: gitConfig.ConfigFilePath, + Ref: gitConfig.ReferenceName, + Hash: commitHash, + }}, + } + } else { templateFolder := strconv.Itoa(customTemplateID) projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent)) @@ -228,11 +250,18 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ } customTemplate.ProjectPath = projectPath + customTemplate.Artifact = nil } - if err := handler.DataStore.CustomTemplate().Update(customTemplate.ID, customTemplate); err != nil { - return httperror.InternalServerError("Unable to persist custom template changes inside the database", err) - } + err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error { + if err := tx.CustomTemplate().Update(customTemplate.ID, customTemplate); err != nil { + return httperror.InternalServerError("Unable to persist custom template changes inside the database", err) + } - return response.JSON(w, customTemplate) + populateGitConfig(tx, customTemplate) + + return nil + }) + + return response.TxResponse(w, customTemplate, err) } diff --git a/api/http/handler/customtemplates/customtemplate_update_test.go b/api/http/handler/customtemplates/customtemplate_update_test.go new file mode 100644 index 000000000..af3262ca3 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_update_test.go @@ -0,0 +1,500 @@ +package customtemplates + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/security" + + "github.com/gorilla/mux" + "github.com/segmentio/encoding/json" + "github.com/stretchr/testify/require" +) + +func updateTemplateRequest(t *testing.T, templateID string, payload any, ctx *security.RestrictedRequestContext) *http.Request { + t.Helper() + + body, err := json.Marshal(payload) + require.NoError(t, err) + + r := httptest.NewRequest(http.MethodPut, "/custom_templates/"+templateID, bytes.NewReader(body)) + r.Header.Set("Content-Type", "application/json") + r = mux.SetURLVars(r, map[string]string{"id": templateID}) + + return r.WithContext(security.StoreRestrictedRequestContext(r, ctx)) +} + +func TestCustomTemplateUpdate_NotFound(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "New Title", + Description: "New Description", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusNotFound, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Forbidden(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "Original Title", + EntryPoint: filesystem.ComposeFileDefaultName, + CreatedByUserID: 1, + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "New Title", + Description: "New Description", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + // User 2 did not create this template and is not an admin + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 2}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusForbidden, herr.StatusCode) +} + +func TestCustomTemplateUpdate_DuplicateTitle(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + require.NoError(t, tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "Template One", + })) + + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 2, + Title: "Template Two", + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "Template One", + Description: "Renamed", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "2", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusInternalServerError, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Success_FileContent(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "Original Title", + Description: "Original Description", + EntryPoint: filesystem.ComposeFileDefaultName, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + CreatedByUserID: 1, + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "Updated Title", + Description: "Updated Description", + FileContent: "version: '3'\nservices:\n app:\n image: alpine", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "Updated Title", tmpl.Title) + require.Equal(t, "Updated Description", tmpl.Description) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(1) + require.NoError(t, err) + require.Equal(t, "Updated Title", stored.Title) + require.Equal(t, "Updated Description", stored.Description) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateUpdate_OwnerCanUpdate(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "User Template", + EntryPoint: filesystem.ComposeFileDefaultName, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + CreatedByUserID: 2, + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "User Template Updated", + Description: "Updated by owner", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + // User 2 is the creator, not an admin + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 2}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Equal(t, "User Template Updated", tmpl.Title) +} + +func TestCustomTemplateUpdate_SameTitleAllowed(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "My Template", + EntryPoint: filesystem.ComposeFileDefaultName, + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + Description: "Updated description", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(1) + require.NoError(t, err) + require.Equal(t, "My Template", stored.Title) + require.Equal(t, "Updated description", stored.Description) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateUpdate_InvalidPayload(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "My Template", + EntryPoint: filesystem.ComposeFileDefaultName, + }) + })) + + payload := customTemplateUpdatePayload{ + // Title is empty - invalid + Description: "A description", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Validation_MissingDescription(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Validation_BothContentAndRepoMissing(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + Description: "A description", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Validation_InvalidPlatform(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + Description: "A description", + FileContent: "version: '3'", + Type: portainer.DockerComposeStack, + Platform: 0, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Validation_InvalidType(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + Description: "A description", + FileContent: "version: '3'", + Type: 0, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Validation_NoteWithImage(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + Description: "A description", + FileContent: "version: '3'", + Note: `Some note `, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_Validation_AuthWithoutCredentials(t *testing.T) { + t.Parallel() + + handler, _, _ := newTestHandler(t) + + payload := customTemplateUpdatePayload{ + Title: "My Template", + Description: "A description", + RepositoryURL: "https://github.com/example/repo", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + RepositoryAuthentication: true, + } + + r := updateTemplateRequest(t, "99", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.NotNil(t, herr) + require.Equal(t, http.StatusBadRequest, herr.StatusCode) +} + +func TestCustomTemplateUpdate_ClearsArtifact(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "Git Template", + EntryPoint: filesystem.ComposeFileDefaultName, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + CreatedByUserID: 1, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{}, + }, + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "Git Template", + Description: "Updated with file content", + FileContent: "version: '3'\nservices:\n app:\n image: alpine", + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.Nil(t, tmpl.Artifact) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(1) + require.NoError(t, err) + require.Nil(t, stored.Artifact) + + return nil + }) + require.NoError(t, err) +} + +func TestCustomTemplateUpdate_GitRepository_Success(t *testing.T) { + t.Parallel() + + handler, ds, _ := newTestHandler(t) + handler.GitService = &gitServiceCreatingFile{} + + projectDir := t.TempDir() + + require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { + return tx.CustomTemplate().Create(&portainer.CustomTemplate{ + ID: 1, + Title: "Git Template", + EntryPoint: filesystem.ComposeFileDefaultName, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + CreatedByUserID: 1, + ProjectPath: projectDir, + }) + })) + + payload := customTemplateUpdatePayload{ + Title: "Git Template", + Description: "Updated via git", + RepositoryURL: "https://github.com/example/repo", + RepositoryReferenceName: "refs/heads/main", + ComposeFilePathInRepository: filesystem.ComposeFileDefaultName, + Type: portainer.DockerComposeStack, + Platform: portainer.CustomTemplatePlatformLinux, + } + + r := updateTemplateRequest(t, "1", payload, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true}) + rr := httptest.NewRecorder() + + herr := handler.customTemplateUpdate(rr, r) + require.Nil(t, herr) + require.Equal(t, http.StatusOK, rr.Code) + + var tmpl portainer.CustomTemplate + require.NoError(t, json.NewDecoder(rr.Body).Decode(&tmpl)) + require.NotNil(t, tmpl.Artifact) + require.Len(t, tmpl.Artifact.Files, 1) + require.Equal(t, "deadbeef123", tmpl.Artifact.Files[0].Hash) + + err := ds.ViewTx(func(tx dataservices.DataStoreTx) error { + stored, err := tx.CustomTemplate().Read(1) + require.NoError(t, err) + require.NotNil(t, stored.Artifact) + + src, err := tx.Source().Read(stored.Artifact.Files[0].SourceID) + require.NoError(t, err) + require.Equal(t, portainer.SourceTypeGit, src.Type) + require.Equal(t, "https://github.com/example/repo", src.Git.URL) + + return nil + }) + require.NoError(t, err) +} diff --git a/api/http/handler/customtemplates/utils.go b/api/http/handler/customtemplates/utils.go index 7ae060d49..721bfabe5 100644 --- a/api/http/handler/customtemplates/utils.go +++ b/api/http/handler/customtemplates/utils.go @@ -2,11 +2,49 @@ package customtemplates import ( "errors" + "regexp" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" ) -func validateVariablesDefinitions(variables []portainer.CustomTemplateVariableDefinition) error { +func populateGitConfig(tx dataservices.DataStoreTx, template *portainer.CustomTemplate) { + if template.Artifact == nil || len(template.Artifact.Files) == 0 { + return + } + + file := template.Artifact.Files[0] + + src, err := tx.Source().Read(file.SourceID) + if err != nil || src.Git == nil { + return + } + + cfg := *src.Git + cfg.ReferenceName = file.Ref + cfg.ConfigFilePath = file.Path + cfg.ConfigHash = file.Hash + + if cfg.Authentication != nil { + sanitized := *cfg.Authentication + sanitized.Password = "" + cfg.Authentication = &sanitized + } + + template.GitConfig = &cfg +} + +// IsValidNote reports whether note is safe to display. Notes containing tags are rejected. +func IsValidNote(note string) bool { + if len(note) == 0 { + return true + } + match, _ := regexp.MatchString(" 0 { + return ErrSourceInUse + } + return tx.Source().Delete(portainer.SourceID(sourceID)) }); h.dataStore.IsErrObjectNotFound(err) { return httperror.NotFound("Unable to find a source with the specified identifier", err) } else if errors.Is(err, ErrSourceInUse) { - return httperror.Conflict("Source is used by one or more workflows", err) + return httperror.Conflict("Source is used by one or more workflows or custom templates", err) } else if err != nil { return httperror.InternalServerError("Unable to delete source", err) } diff --git a/api/http/handler/gitops/sources/delete_test.go b/api/http/handler/gitops/sources/delete_test.go index f148e962e..49ba8fac5 100644 --- a/api/http/handler/gitops/sources/delete_test.go +++ b/api/http/handler/gitops/sources/delete_test.go @@ -14,6 +14,7 @@ import ( func TestSourceDelete_Success(t *testing.T) { t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) var srcID portainer.SourceID @@ -35,6 +36,7 @@ func TestSourceDelete_Success(t *testing.T) { func TestSourceDelete_NotFound(t *testing.T) { t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { @@ -50,6 +52,7 @@ func TestSourceDelete_NotFound(t *testing.T) { func TestSourceDelete_InUse(t *testing.T) { t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) var srcID portainer.SourceID @@ -59,7 +62,7 @@ func TestSourceDelete_InUse(t *testing.T) { require.NoError(t, err) srcID = src.ID - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{src.ID}}}} + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}} err = tx.Workflow().Create(wf) require.NoError(t, err) @@ -75,6 +78,7 @@ func TestSourceDelete_InUse(t *testing.T) { func TestSourceDelete_NonNumericID(t *testing.T) { t.Parallel() + _, store := datastore.MustNewTestStore(t, false, true) require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { @@ -87,3 +91,34 @@ func TestSourceDelete_NonNumericID(t *testing.T) { require.Equal(t, http.StatusBadRequest, rr.Code) } + +func TestSourceDelete_InUseByCustomTemplate(t *testing.T) { + t.Parallel() + + _, store := datastore.MustNewTestStore(t, false, true) + + var srcID portainer.SourceID + require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { + src := &portainer.Source{Name: "in-use-by-template", Type: portainer.SourceTypeGit} + err := tx.Source().Create(src) + require.NoError(t, err) + srcID = src.ID + + ct := &portainer.CustomTemplate{ + ID: 1, + Artifact: &portainer.Artifact{ + Files: []portainer.ArtifactFile{{SourceID: src.ID}}, + }, + } + err = tx.CustomTemplate().Create(ct) + require.NoError(t, err) + + return tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole}) + })) + + h := newTestHandler(t, store) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, buildDeleteReq(t, 1, int(srcID))) + + require.Equal(t, http.StatusConflict, rr.Code) +} diff --git a/api/http/handler/gitops/sources/fetch.go b/api/http/handler/gitops/sources/fetch.go index 654ff02f7..c97af8f26 100644 --- a/api/http/handler/gitops/sources/fetch.go +++ b/api/http/handler/gitops/sources/fetch.go @@ -13,8 +13,10 @@ import ( // FetchSourceWorkflows returns the workflows and stats for a single source. func FetchSourceWorkflows(tx dataservices.DataStoreTx, src *portainer.Source) ([]ce.Workflow, ce.SourceStats, error) { wfs, err := tx.Workflow().ReadAll(func(wf portainer.Workflow) bool { - return slices.ContainsFunc(wf.Artifacts, func(artifact portainer.ArtifactSources) bool { - return slices.Contains(artifact.SourceIDs, src.ID) + return slices.ContainsFunc(wf.Artifacts, func(artifact portainer.Artifact) bool { + return slices.ContainsFunc(artifact.Files, func(f portainer.ArtifactFile) bool { + return f.SourceID == src.ID + }) }) }) if err != nil { @@ -40,7 +42,7 @@ func FetchSourceWorkflows(tx dataservices.DataStoreTx, src *portainer.Source) ([ stats := ce.SourceStats{EndpointIDs: set.Set[portainer.EndpointID]{}} for _, stacks := range stacks { - items = append(items, ce.MapStackToWorkflow(stacks, src.GitConfig, unknown, unknown)) + items = append(items, ce.MapStackToWorkflow(stacks, src.Git, unknown, unknown)) stats.WorkflowCount++ if stacks.EndpointID != 0 { stats.EndpointIDs.Add(stacks.EndpointID) diff --git a/api/http/handler/gitops/sources/get.go b/api/http/handler/gitops/sources/get.go index 07e7848e5..ebb7c6b86 100644 --- a/api/http/handler/gitops/sources/get.go +++ b/api/http/handler/gitops/sources/get.go @@ -80,7 +80,7 @@ func (h *Handler) getSource(w http.ResponseWriter, r *http.Request) *httperror.H return httperror.InternalServerError("Unable to retrieve source", err) } - detail := BuildSourceDetail(h.buildSource(r.Context(), source, stats), source.GitConfig, sourceWfs) + detail := BuildSourceDetail(h.buildSource(r.Context(), source, stats), source.Git, sourceWfs) return response.JSON(w, detail) } diff --git a/api/http/handler/gitops/sources/helpers_test.go b/api/http/handler/gitops/sources/helpers_test.go index 6acb04975..5a0780cd1 100644 --- a/api/http/handler/gitops/sources/helpers_test.go +++ b/api/http/handler/gitops/sources/helpers_test.go @@ -23,20 +23,20 @@ func createGitWorkflow(t *testing.T, tx dataservices.DataStoreTx, stack *portain t.Helper() src := &portainer.Source{ - Name: gittypes.RepoName(cfg.URL), - Type: portainer.SourceTypeGit, - GitConfig: cfg, + Name: gittypes.RepoName(cfg.URL), + Type: portainer.SourceTypeGit, + Git: cfg, } require.NoError(t, tx.Source().Create(src)) wf := &portainer.Workflow{ - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - StackID: stack.ID, - ReferenceName: cfg.ReferenceName, - ConfigFilePath: cfg.ConfigFilePath, - }, - SourceIDs: []portainer.SourceID{src.ID}, + Artifacts: []portainer.Artifact{{ + StackID: stack.ID, + Files: []portainer.ArtifactFile{{ + SourceID: src.ID, + Path: cfg.ConfigFilePath, + Ref: cfg.ReferenceName, + }}, }}, } require.NoError(t, tx.Workflow().Create(wf)) diff --git a/api/http/handler/gitops/sources/list_test.go b/api/http/handler/gitops/sources/list_test.go index f81097520..7d8c40117 100644 --- a/api/http/handler/gitops/sources/list_test.go +++ b/api/http/handler/gitops/sources/list_test.go @@ -19,12 +19,13 @@ func TestSourcesList_GroupsByURLAndCredentials(t *testing.T) { require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { cfg := gitCfg("https://github.com/org/repo") - src := &portainer.Source{Name: "repo", Type: portainer.SourceTypeGit, GitConfig: cfg} + src := &portainer.Source{Name: "repo", Type: portainer.SourceTypeGit, Git: cfg} require.NoError(t, tx.Source().Create(src)) - wfA := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{src.ID}}}} + wfA := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}} require.NoError(t, tx.Workflow().Create(wfA)) - wfB := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{src.ID}}}} + + wfB := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}} require.NoError(t, tx.Workflow().Create(wfB)) require.NoError(t, tx.Stack().Create(&portainer.Stack{ID: 1, Name: "stack-a", WorkflowID: wfA.ID})) diff --git a/api/http/handler/gitops/sources/source_connection.go b/api/http/handler/gitops/sources/source_connection.go index 3bff390a9..ae8b68aea 100644 --- a/api/http/handler/gitops/sources/source_connection.go +++ b/api/http/handler/gitops/sources/source_connection.go @@ -58,11 +58,11 @@ func (h *Handler) sourceTestConnection(w http.ResponseWriter, r *http.Request) * return httperror.InternalServerError("Unable to apply source changes", err) } - if src.GitConfig == nil { + if src.Git == nil { return httperror.InternalServerError("Source has no git configuration", nil) } - result := testSourceConnection(r.Context(), h.gitService, src.GitConfig) + result := testSourceConnection(r.Context(), h.gitService, src.Git) return response.JSON(w, result) } diff --git a/api/http/handler/gitops/sources/update_git.go b/api/http/handler/gitops/sources/update_git.go index 54cd50c3e..f6c2a725f 100644 --- a/api/http/handler/gitops/sources/update_git.go +++ b/api/http/handler/gitops/sources/update_git.go @@ -104,7 +104,7 @@ func (h *Handler) gitSourceUpdate(w http.ResponseWriter, r *http.Request) *httpe return httperror.InternalServerError("Unable to update source", err) } - src.GitConfig = gittypes.SanitizeRepoConfig(src.GitConfig) + src.Git = gittypes.SanitizeRepoConfig(src.Git) return response.JSON(w, src) } @@ -119,7 +119,7 @@ func ApplyGitSourceChanges(src *portainer.Source, payload GitSourceUpdatePayload src.Name = *payload.Name } - gitConfig := src.GitConfig + gitConfig := src.Git if gitConfig == nil { gitConfig = &gittypes.RepoConfig{} } @@ -167,7 +167,7 @@ func ApplyGitSourceChanges(src *portainer.Source, payload GitSourceUpdatePayload } gitConfig.Authentication = auth - src.GitConfig = gitConfig + src.Git = gitConfig return nil } diff --git a/api/http/handler/gitops/sources/update_git_test.go b/api/http/handler/gitops/sources/update_git_test.go index 12d72e3d4..31ff2f58f 100644 --- a/api/http/handler/gitops/sources/update_git_test.go +++ b/api/http/handler/gitops/sources/update_git_test.go @@ -45,8 +45,8 @@ func TestGitSourceUpdate_Success(t *testing.T) { err = json.NewDecoder(rr.Body).Decode(&src) require.NoError(t, err) require.Equal(t, "new-name", src.Name) - require.NotNil(t, src.GitConfig) - require.Equal(t, "https://github.com/org/new.git", src.GitConfig.URL) + require.NotNil(t, src.Git) + require.Equal(t, "https://github.com/org/new.git", src.Git.URL) } func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) { @@ -58,7 +58,7 @@ func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) { src := &portainer.Source{ Name: "auth-source", Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/org/repo.git", Authentication: &gittypes.GitAuthentication{ Username: "alice", @@ -92,10 +92,10 @@ func TestGitSourceUpdate_PreservesAuthWhenNotProvided(t *testing.T) { stored, err = tx.Source().Read(srcID) return err })) - require.NotNil(t, stored.GitConfig) - require.NotNil(t, stored.GitConfig.Authentication) - require.Equal(t, "alice", stored.GitConfig.Authentication.Username) - require.Equal(t, "secret", stored.GitConfig.Authentication.Password) + require.NotNil(t, stored.Git) + require.NotNil(t, stored.Git.Authentication) + require.Equal(t, "alice", stored.Git.Authentication.Username) + require.Equal(t, "secret", stored.Git.Authentication.Password) } func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) { @@ -107,7 +107,7 @@ func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) { src := &portainer.Source{ Name: "auth-source", Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/org/repo.git", Authentication: &gittypes.GitAuthentication{ Username: "alice", @@ -141,8 +141,8 @@ func TestGitSourceUpdate_ClearsAuthWhenRequested(t *testing.T) { stored, err = tx.Source().Read(srcID) return err })) - require.NotNil(t, stored.GitConfig) - require.Nil(t, stored.GitConfig.Authentication) + require.NotNil(t, stored.Git) + require.Nil(t, stored.Git.Authentication) } func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) { @@ -154,7 +154,7 @@ func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) { src := &portainer.Source{ Name: "auth-source", Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/org/repo.git", Authentication: &gittypes.GitAuthentication{ Username: "alice", @@ -191,10 +191,10 @@ func TestGitSourceUpdate_ReplacesAuthWhenProvided(t *testing.T) { stored, err = tx.Source().Read(srcID) return err })) - require.NotNil(t, stored.GitConfig) - require.NotNil(t, stored.GitConfig.Authentication) - require.Equal(t, "bob", stored.GitConfig.Authentication.Username) - require.Equal(t, "new-secret", stored.GitConfig.Authentication.Password) + require.NotNil(t, stored.Git) + require.NotNil(t, stored.Git.Authentication) + require.Equal(t, "bob", stored.Git.Authentication.Username) + require.Equal(t, "new-secret", stored.Git.Authentication.Password) } func TestGitSourceUpdate_NotFound(t *testing.T) { @@ -225,7 +225,7 @@ func TestGitSourceUpdate_ConflictOnDuplicateURL(t *testing.T) { existing := &portainer.Source{ Name: "existing", Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/org/existing.git", }, } diff --git a/api/http/handler/gitops/sources/utils.go b/api/http/handler/gitops/sources/utils.go index 9dce04bb3..5811ad7b0 100644 --- a/api/http/handler/gitops/sources/utils.go +++ b/api/http/handler/gitops/sources/utils.go @@ -11,8 +11,8 @@ import ( func (h *Handler) buildSource(ctx context.Context, src *portainer.Source, stats ce.SourceStats) Source { var status ce.Status var sourceErr string - if src.GitConfig != nil { - phase, _ := ce.ComputeGitPhasesForConfig(ctx, h.gitService, src.GitConfig) + if src.Git != nil { + phase, _ := ce.ComputeGitPhasesForConfig(ctx, h.gitService, src.Git) status = phase.Status sourceErr = phase.Error } else { @@ -21,10 +21,10 @@ func (h *Handler) buildSource(ctx context.Context, src *portainer.Source, stats url := "" var provider gittypes.GitProvider - if src.GitConfig != nil { - url = gittypes.SanitizeURL(src.GitConfig.URL) - if src.GitConfig.Authentication != nil { - provider = src.GitConfig.Authentication.Provider + if src.Git != nil { + url = gittypes.SanitizeURL(src.Git.URL) + if src.Git.Authentication != nil { + provider = src.Git.Authentication.Provider } } diff --git a/api/http/handler/gitops/workflows/helpers_test.go b/api/http/handler/gitops/workflows/helpers_test.go index 289ed877d..73b53dc8e 100644 --- a/api/http/handler/gitops/workflows/helpers_test.go +++ b/api/http/handler/gitops/workflows/helpers_test.go @@ -47,17 +47,17 @@ func createGitStack(t *testing.T, tx dataservices.DataStoreTx, stack *portainer. t.Helper() if stack.GitConfig != nil { - src := &portainer.Source{GitConfig: stack.GitConfig, Type: portainer.SourceTypeGit} + src := &portainer.Source{Git: stack.GitConfig, Type: portainer.SourceTypeGit} require.NoError(t, tx.Source().Create(src)) - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - StackID: stack.ID, - ReferenceName: stack.GitConfig.ReferenceName, - ConfigFilePath: stack.GitConfig.ConfigFilePath, - ConfigHash: stack.GitConfig.ConfigHash, - }, - SourceIDs: []portainer.SourceID{src.ID}, + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{ + StackID: stack.ID, + Files: []portainer.ArtifactFile{{ + SourceID: src.ID, + Path: stack.GitConfig.ConfigFilePath, + Ref: stack.GitConfig.ReferenceName, + Hash: stack.GitConfig.ConfigHash, + }}, }}} require.NoError(t, tx.Workflow().Create(wf)) diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 841a8b488..81a9f586c 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -20,11 +20,12 @@ func hideFields(settings *portainer.Settings) { // Handler is the HTTP handler used to handle settings operations. type Handler struct { *mux.Router - DataStore dataservices.DataStore - FileService portainer.FileService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - SnapshotService portainer.SnapshotService + DataStore dataservices.DataStore + FileService portainer.FileService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + SnapshotService portainer.SnapshotService + SetupTokenRequired bool } // NewHandler creates a handler to manage settings operations. diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 7267d94da..3943961d3 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -44,6 +44,8 @@ type publicSettingsResponse struct { } IsDockerDesktopExtension bool `json:"IsDockerDesktopExtension" example:"false"` + // Whether the setup wizard must send the X-Setup-Token header for admin init / restore + RequiresSetupToken bool `json:"RequiresSetupToken" example:"false"` } // @id SettingsPublic @@ -62,6 +64,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * } publicSettings := generatePublicSettings(settings) + publicSettings.RequiresSetupToken = handler.SetupTokenRequired return response.JSON(w, publicSettings) } diff --git a/api/http/handler/stacks/response.go b/api/http/handler/stacks/response.go index 5f0316031..e0eafc5e3 100644 --- a/api/http/handler/stacks/response.go +++ b/api/http/handler/stacks/response.go @@ -10,12 +10,12 @@ import ( // loadGitConfigForStack reads the merged GitConfig (Source URL/auth/TLS + Artifact ref/path/hash) // and the SourceID for the given stack. func loadGitConfigForStack(tx dataservices.DataStoreTx, workflowID portainer.WorkflowID, stackID portainer.StackID) (*gittypes.RepoConfig, portainer.SourceID, error) { - src, artifact, err := workflows.GitSourceAndArtifactForStack(tx, workflowID, stackID) + src, file, err := workflows.GitSourceAndArtifactForStack(tx, workflowID, stackID) if err != nil || src == nil { return nil, 0, err } - return workflows.MergeSourceAndArtifact(src, artifact), src.ID, nil + return workflows.MergeSourceAndFile(src, file), src.ID, nil } func saveStackGitConfig(tx dataservices.DataStoreTx, workflowID portainer.WorkflowID, stackID portainer.StackID, oldSourceID portainer.SourceID, cfg *gittypes.RepoConfig) error { diff --git a/api/http/handler/stacks/stack_file_test.go b/api/http/handler/stacks/stack_file_test.go index 5a931017d..fe1afed45 100644 --- a/api/http/handler/stacks/stack_file_test.go +++ b/api/http/handler/stacks/stack_file_test.go @@ -40,16 +40,16 @@ func TestStackFile_GitPendingRedeploy_Returns409(t *testing.T) { src := &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "https://github.com/portainer/portainer.git", ConfigFilePath: "docker-compose.yml", }, } require.NoError(t, store.Source().Create(src)) - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: stackID}, - SourceIDs: []portainer.SourceID{src.ID}, + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{ + StackID: stackID, + Files: []portainer.ArtifactFile{{SourceID: src.ID}}, }}} require.NoError(t, store.Workflow().Create(wf)) diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index 6f15e8d9c..447642db8 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -18,8 +18,8 @@ import ( ) // @id StackStop -// @summary Stops a stopped Stack -// @description Stops a stopped Stack. +// @summary Stop a running Stack +// @description Stop a running Stack. // @description **Access policy**: authenticated // @tags stacks // @security ApiKeyAuth diff --git a/api/http/handler/stacks/stack_update_git_redeploy.go b/api/http/handler/stacks/stack_update_git_redeploy.go index ff9be2597..65304bdbd 100644 --- a/api/http/handler/stacks/stack_update_git_redeploy.go +++ b/api/http/handler/stacks/stack_update_git_redeploy.go @@ -236,8 +236,8 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request) return err } - return workflows.UpdateArtifactForStack(tx, stack.WorkflowID, stack.ID, func(a *portainer.Artifact) { - a.ConfigHash = oldConfigHash + return workflows.UpdateArtifactFileForStack(tx, stack.WorkflowID, stack.ID, sourceID, func(a *portainer.ArtifactFile) { + a.Hash = oldConfigHash }) }); err != nil { log.Error().Err(err).Int("stack_id", int(stack.ID)).Msg("failed to revert config hash after failed redeploy") diff --git a/api/http/handler/stacks/stack_update_git_test.go b/api/http/handler/stacks/stack_update_git_test.go index 786acfa8d..576160173 100644 --- a/api/http/handler/stacks/stack_update_git_test.go +++ b/api/http/handler/stacks/stack_update_git_test.go @@ -37,29 +37,29 @@ func TestStackUpdateGitWebhookUniqueness(t *testing.T) { const stack2ID = portainer.StackID(457) src1 := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"}, } err = store.Source().Create(src1) require.NoError(t, err) - wf1 := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: stack1ID}, - SourceIDs: []portainer.SourceID{src1.ID}, + wf1 := &portainer.Workflow{Artifacts: []portainer.Artifact{{ + StackID: stack1ID, + Files: []portainer.ArtifactFile{{SourceID: src1.ID}}, }}} err = store.Workflow().Create(wf1) require.NoError(t, err) src2 := &portainer.Source{ - Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"}, + Type: portainer.SourceTypeGit, + Git: &gittypes.RepoConfig{URL: "https://github.com/portainer/portainer.git"}, } err = store.Source().Create(src2) require.NoError(t, err) - wf2 := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: stack2ID}, - SourceIDs: []portainer.SourceID{src2.ID}, + wf2 := &portainer.Workflow{Artifacts: []portainer.Artifact{{ + StackID: stack2ID, + Files: []portainer.ArtifactFile{{SourceID: src2.ID}}, }}} err = store.Workflow().Create(wf2) require.NoError(t, err) diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index f7d75931f..dd31f7427 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -6,6 +6,7 @@ import ( "strings" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security/setuptoken" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/response" @@ -31,17 +32,23 @@ func (payload *adminInitPayload) Validate(r *http.Request) error { // @id UserAdminInit // @summary Initialize administrator account // @description Initialize the 'admin' user account. -// @description **Access policy**: public +// @description **Access policy**: public (requires the X-Setup-Token header on an uninitialized instance unless --no-setup-token is set) // @tags users // @accept json // @produce json +// @param X-Setup-Token header string false "Setup token (required when instance is uninitialized and --no-setup-token is not set)" // @param body body adminInitPayload true "User details" // @success 200 {object} portainer.User "Success" // @failure 400 "Invalid request" +// @failure 403 "Access denied - invalid or missing setup token" // @failure 409 "Admin user already initialized" // @failure 500 "Server error" // @router /users/admin/init [post] func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + if err := setuptoken.Validate(r, handler.SetupToken); err != nil { + return err + } + var payload adminInitPayload err := request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { diff --git a/api/http/handler/users/admin_init_test.go b/api/http/handler/users/admin_init_test.go new file mode 100644 index 000000000..62691495d --- /dev/null +++ b/api/http/handler/users/admin_init_test.go @@ -0,0 +1,65 @@ +package users + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/portainer/portainer/api/apikey" + "github.com/portainer/portainer/api/datastore" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/http/security/setuptoken" + "github.com/portainer/portainer/api/internal/testhelpers" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newAdminInitHandler(t *testing.T) *Handler { + t.Helper() + _, store := datastore.MustNewTestStore(t, true, false) + rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour) + apiKeyService := apikey.NewAPIKeyService(store.APIKeyRepository(), store.User()) + h := NewHandler(testhelpers.NewTestRequestBouncer(), rateLimiter, apiKeyService, mockPasswordStrengthChecker{}) + h.DataStore = store + h.CryptoService = testhelpers.NewCryptoService() + h.AdminCreationDone = make(chan struct{}, 1) + return h +} + +func Test_adminInit_setupTokenGate(t *testing.T) { + t.Parallel() + + t.Run("403 without token header", func(t *testing.T) { + handler := newAdminInitHandler(t) + handler.SetupToken = "secret-token" + body := strings.NewReader(`{"Username":"admin","Password":"abcdefgh12"}`) + r := httptest.NewRequest(http.MethodPost, "/users/admin/init", body) + err := handler.adminInit(httptest.NewRecorder(), r) + require.NotNil(t, err) + assert.Equal(t, http.StatusForbidden, err.StatusCode) + }) + + t.Run("403 with wrong token", func(t *testing.T) { + handler := newAdminInitHandler(t) + handler.SetupToken = "secret-token" + body := strings.NewReader(`{"Username":"admin","Password":"abcdefgh12"}`) + r := httptest.NewRequest(http.MethodPost, "/users/admin/init", body) + r.Header.Set(setuptoken.HeaderName, "wrong") + err := handler.adminInit(httptest.NewRecorder(), r) + require.NotNil(t, err) + assert.Equal(t, http.StatusForbidden, err.StatusCode) + }) + + t.Run("succeeds with correct token", func(t *testing.T) { + handler := newAdminInitHandler(t) + handler.SetupToken = "secret-token" + body := strings.NewReader(`{"Username":"admin","Password":"abcdefgh12"}`) + r := httptest.NewRequest(http.MethodPost, "/users/admin/init", body) + r.Header.Set(setuptoken.HeaderName, "secret-token") + err := handler.adminInit(httptest.NewRecorder(), r) + assert.Nil(t, err) + }) +} diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index 60af6e05a..1c2d6eb3a 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -35,6 +35,7 @@ type Handler struct { passwordStrengthChecker security.PasswordStrengthChecker AdminCreationDone chan<- struct{} FileService portainer.FileService + SetupToken string } // NewHandler creates a handler to manage user operations. diff --git a/api/http/proxy/factory/agent.go b/api/http/proxy/factory/agent.go index 821b7ada0..ac6d6892e 100644 --- a/api/http/proxy/factory/agent.go +++ b/api/http/proxy/factory/agent.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/portainer/api/http/proxy/factory/agent" "github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/url" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -40,21 +41,30 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy } endpointURL.Scheme = "http" - httpTransport := &http.Transport{} + var innerTransport *http.Transport if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { - config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig) + tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig) if err != nil { return nil, errors.WithMessage(err, "failed generating tls configuration") } - httpTransport.TLSClientConfig = config endpointURL.Scheme = "https" + + if endpointutils.IsEdgeEndpoint(endpoint) { + innerTransport = ssrf.WrapTransportInternal(&http.Transport{TLSClientConfig: tlsConfig}) + } else { + innerTransport = ssrf.WrapTransport(&http.Transport{TLSClientConfig: tlsConfig}) + } + } else if endpointutils.IsEdgeEndpoint(endpoint) { + innerTransport = ssrf.WrapTransportInternal(&http.Transport{}) + } else { + innerTransport = ssrf.WrapTransport(&http.Transport{}) } proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL) - proxy.Transport = agent.NewTransport(factory.signatureService, httpTransport) + proxy.Transport = agent.NewTransport(factory.signatureService, innerTransport) proxyServer := &ProxyServer{ server: &http.Server{ diff --git a/api/http/proxy/factory/docker.go b/api/http/proxy/factory/docker.go index 0db22aa4c..f49b09374 100644 --- a/api/http/proxy/factory/docker.go +++ b/api/http/proxy/factory/docker.go @@ -8,8 +8,10 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/proxy/factory/docker" + "github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/url" httperror "github.com/portainer/portainer/pkg/libhttp/error" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/rs/zerolog/log" ) @@ -47,17 +49,6 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h } endpointURL.Scheme = "http" - httpTransport := &http.Transport{} - - if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { - config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig) - if err != nil { - return nil, err - } - - httpTransport.TLSClientConfig = config - endpointURL.Scheme = "https" - } transportParameters := &docker.TransportParameters{ Endpoint: endpoint, @@ -67,7 +58,27 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h DockerClientFactory: factory.dockerClientFactory, } - dockerTransport, err := docker.NewTransport(transportParameters, httpTransport, factory.gitService, factory.snapshotService) + var innerTransport *http.Transport + if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { + tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "https" + + if endpointutils.IsEdgeEndpoint(endpoint) { + innerTransport = ssrf.WrapTransportInternal(&http.Transport{TLSClientConfig: tlsConfig}) + } else { + innerTransport = ssrf.WrapTransport(&http.Transport{TLSClientConfig: tlsConfig}) + } + } else if endpointutils.IsEdgeEndpoint(endpoint) { + innerTransport = ssrf.WrapTransportInternal(&http.Transport{}) + } else { + innerTransport = ssrf.WrapTransport(&http.Transport{}) + } + + dockerTransport, err := docker.NewTransport(transportParameters, innerTransport, factory.gitService, factory.snapshotService) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/github/client.go b/api/http/proxy/factory/github/client.go index e075e12b9..d455d79f2 100644 --- a/api/http/proxy/factory/github/client.go +++ b/api/http/proxy/factory/github/client.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/portainer/portainer/pkg/libhttp/ssrf" + "github.com/rs/zerolog/log" "github.com/segmentio/encoding/json" "oras.land/oras-go/v2/registry/remote/retry" @@ -92,7 +94,7 @@ func NewHTTPClient(token string) *http.Client { return &http.Client{ Transport: &tokenTransport{ token: token, - transport: retry.NewTransport(&http.Transport{}), // Use ORAS retry transport for consistent rate limiting and error handling + transport: retry.NewTransport(ssrf.WrapTransport(&http.Transport{})), // Use ORAS retry transport for consistent rate limiting and error handling }, Timeout: 1 * time.Minute, } diff --git a/api/http/proxy/factory/gitlab/client.go b/api/http/proxy/factory/gitlab/client.go index 050dab881..d4deb18f3 100644 --- a/api/http/proxy/factory/gitlab/client.go +++ b/api/http/proxy/factory/gitlab/client.go @@ -8,6 +8,8 @@ import ( "net/http" "time" + "github.com/portainer/portainer/pkg/libhttp/ssrf" + "github.com/rs/zerolog/log" "github.com/segmentio/encoding/json" "oras.land/oras-go/v2/registry/remote/retry" @@ -92,7 +94,7 @@ type Transport struct { // interface for proxying requests to the Gitlab API. func NewTransport() *Transport { return &Transport{ - httpTransport: &http.Transport{}, + httpTransport: ssrf.WrapTransport(&http.Transport{}), } } @@ -117,7 +119,7 @@ func NewHTTPClient(token string) *http.Client { return &http.Client{ Transport: &tokenTransport{ token: token, - transport: retry.NewTransport(&http.Transport{}), // Use ORAS retry transport for consistent rate limiting and error handling + transport: retry.NewTransport(ssrf.WrapTransport(&http.Transport{})), // Use ORAS retry transport for consistent rate limiting and error handling }, Timeout: 1 * time.Minute, } diff --git a/api/http/proxy/factory/kubernetes/agent_transport.go b/api/http/proxy/factory/kubernetes/agent_transport.go index 4a62e2367..c8232b1b8 100644 --- a/api/http/proxy/factory/kubernetes/agent_transport.go +++ b/api/http/proxy/factory/kubernetes/agent_transport.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/portainer/portainer/pkg/libhttp/ssrf" ) type agentTransport struct { @@ -24,9 +25,9 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, token transport := &agentTransport{ baseTransport: newBaseTransport( - &http.Transport{ + ssrf.WrapTransport(&http.Transport{ TLSClientConfig: tlsConfig, - }, + }), tokenManager, endpoint, k8sClientFactory, diff --git a/api/http/proxy/factory/kubernetes/edge_transport.go b/api/http/proxy/factory/kubernetes/edge_transport.go index 73946114e..cd817eb6b 100644 --- a/api/http/proxy/factory/kubernetes/edge_transport.go +++ b/api/http/proxy/factory/kubernetes/edge_transport.go @@ -7,6 +7,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/portainer/portainer/pkg/libhttp/ssrf" ) type edgeTransport struct { @@ -21,7 +22,7 @@ func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portain reverseTunnelService: reverseTunnelService, signatureService: signatureService, baseTransport: newBaseTransport( - &http.Transport{}, + ssrf.WrapTransportInternal(&http.Transport{}), tokenManager, endpoint, k8sClientFactory, diff --git a/api/http/proxy/factory/kubernetes/edge_transport_test.go b/api/http/proxy/factory/kubernetes/edge_transport_test.go new file mode 100644 index 000000000..ddf40d839 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/edge_transport_test.go @@ -0,0 +1,14 @@ +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewEdgeTransport(t *testing.T) { + t.Parallel() + + transport := NewEdgeTransport(nil, nil, nil, nil, nil, nil, nil) + require.NotNil(t, transport) +} diff --git a/api/http/proxy/factory/kubernetes/local_transport.go b/api/http/proxy/factory/kubernetes/local_transport.go index bc832f35c..6c22c5dc5 100644 --- a/api/http/proxy/factory/kubernetes/local_transport.go +++ b/api/http/proxy/factory/kubernetes/local_transport.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/portainer/portainer/pkg/libhttp/ssrf" ) type localTransport struct { @@ -22,9 +23,9 @@ func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, transport := &localTransport{ baseTransport: newBaseTransport( - &http.Transport{ + ssrf.WrapTransportInternal(&http.Transport{ TLSClientConfig: config, - }, + }), tokenManager, endpoint, k8sClientFactory, diff --git a/api/http/proxy/factory/transport_test.go b/api/http/proxy/factory/transport_test.go new file mode 100644 index 000000000..d6c623a66 --- /dev/null +++ b/api/http/proxy/factory/transport_test.go @@ -0,0 +1,200 @@ +package factory + +import ( + "context" + "net/http/httputil" + "testing" + "time" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/docker" + "github.com/portainer/portainer/pkg/fips" + "github.com/portainer/portainer/pkg/libhttp/ssrf" + + "github.com/stretchr/testify/require" +) + +func init() { + fips.InitFIPS(false) +} + +type stubTunnelService struct{} + +func (s *stubTunnelService) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error { + return nil +} + +func (s *stubTunnelService) StopTunnelServer() error { return nil } + +func (s *stubTunnelService) GenerateEdgeKey(apiURL, tunnelAddr string, endpointIdentifier int) string { + return "" +} + +func (s *stubTunnelService) Open(endpoint *portainer.Endpoint) error { return nil } + +func (s *stubTunnelService) Config(endpointID portainer.EndpointID) portainer.TunnelDetails { + return portainer.TunnelDetails{} +} + +func (s *stubTunnelService) TunnelAddr(endpoint *portainer.Endpoint) (string, error) { + return "127.0.0.1:9999", nil +} + +func (s *stubTunnelService) UpdateLastActivity(endpointID portainer.EndpointID) {} + +func (s *stubTunnelService) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxKeepAlive time.Duration) { +} + +func enableSSRF(t *testing.T) { + t.Helper() + ssrf.Configure(ssrf.Policy{Mode: ssrf.ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { ssrf.Configure(ssrf.Policy{}) }) +} + +// TestNewDockerHTTPProxy_NonEdgeNoTLS verifies that a plain non-edge endpoint +// uses WrapTransport, setting DialContext on the inner transport. +func TestNewDockerHTTPProxy_NonEdgeNoTLS(t *testing.T) { + enableSSRF(t) + + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.DockerEnvironment, + URL: "tcp://192.168.1.100:2376", + } + + handler, err := f.newDockerHTTPProxy(endpoint) + require.NoError(t, err) + + proxy := handler.(*httputil.ReverseProxy) + dt := proxy.Transport.(*docker.Transport) + require.NotNil(t, dt.HTTPTransport.DialContext) +} + +// TestNewDockerHTTPProxy_NonEdgeTLS verifies that a TLS non-edge endpoint +// uses WrapTransport, setting DialContext on the inner transport. +func TestNewDockerHTTPProxy_NonEdgeTLS(t *testing.T) { + enableSSRF(t) + + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.DockerEnvironment, + URL: "tcp://192.168.1.100:2376", + TLSConfig: portainer.TLSConfiguration{ + TLS: true, + TLSSkipVerify: true, + }, + } + + handler, err := f.newDockerHTTPProxy(endpoint) + require.NoError(t, err) + + proxy := handler.(*httputil.ReverseProxy) + dt := proxy.Transport.(*docker.Transport) + require.NotNil(t, dt.HTTPTransport.DialContext) +} + +// TestNewDockerHTTPProxy_EdgeNoTLS verifies that an edge endpoint without TLS +// uses WrapTransportInternal, leaving DialContext nil. +func TestNewDockerHTTPProxy_EdgeNoTLS(t *testing.T) { + enableSSRF(t) + + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "tcp://192.168.1.100:2376", + } + + handler, err := f.newDockerHTTPProxy(endpoint) + require.NoError(t, err) + + proxy := handler.(*httputil.ReverseProxy) + dt := proxy.Transport.(*docker.Transport) + require.Nil(t, dt.HTTPTransport.DialContext) +} + +// TestNewDockerHTTPProxy_EdgeTLS verifies that an edge endpoint with TLS +// uses WrapTransportInternal, leaving DialContext nil. +func TestNewDockerHTTPProxy_EdgeTLS(t *testing.T) { + enableSSRF(t) + + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "tcp://192.168.1.100:2376", + TLSConfig: portainer.TLSConfiguration{ + TLS: true, + TLSSkipVerify: true, + }, + } + + handler, err := f.newDockerHTTPProxy(endpoint) + require.NoError(t, err) + + proxy := handler.(*httputil.ReverseProxy) + dt := proxy.Transport.(*docker.Transport) + require.Nil(t, dt.HTTPTransport.DialContext) +} + +func TestNewAgentProxy_NonEdgeNoTLS(t *testing.T) { + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.AgentOnDockerEnvironment, + URL: "tcp://192.168.1.100:9001", + } + + proxyServer, err := f.NewAgentProxy(endpoint) + require.NoError(t, err) + defer proxyServer.Close() + + require.Positive(t, proxyServer.Port) +} + +func TestNewAgentProxy_NonEdgeTLS(t *testing.T) { + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.AgentOnDockerEnvironment, + URL: "tcp://192.168.1.100:9001", + TLSConfig: portainer.TLSConfiguration{ + TLS: true, + TLSSkipVerify: true, + }, + } + + proxyServer, err := f.NewAgentProxy(endpoint) + require.NoError(t, err) + defer proxyServer.Close() + + require.Positive(t, proxyServer.Port) +} + +func TestNewAgentProxy_EdgeNoTLS(t *testing.T) { + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "tcp://192.168.1.100:9001", + } + + proxyServer, err := f.NewAgentProxy(endpoint) + require.NoError(t, err) + defer proxyServer.Close() + + require.Positive(t, proxyServer.Port) +} + +func TestNewAgentProxy_EdgeTLS(t *testing.T) { + f := &ProxyFactory{reverseTunnelService: &stubTunnelService{}} + endpoint := &portainer.Endpoint{ + Type: portainer.EdgeAgentOnDockerEnvironment, + URL: "tcp://192.168.1.100:9001", + TLSConfig: portainer.TLSConfiguration{ + TLS: true, + TLSSkipVerify: true, + }, + } + + proxyServer, err := f.NewAgentProxy(endpoint) + require.NoError(t, err) + defer proxyServer.Close() + + require.Positive(t, proxyServer.Port) +} diff --git a/api/http/security/setuptoken/setuptoken.go b/api/http/security/setuptoken/setuptoken.go new file mode 100644 index 000000000..ec1af9927 --- /dev/null +++ b/api/http/security/setuptoken/setuptoken.go @@ -0,0 +1,48 @@ +// Package setuptoken provides a one-time setup token used to protect the +// public initialization endpoints (admin account creation and backup restore) +// on an uninitialized Portainer instance. +package setuptoken + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/hex" + "errors" + "net/http" + + httperror "github.com/portainer/portainer/pkg/libhttp/error" +) + +// HeaderName is the HTTP header that carries the setup token. +const HeaderName = "X-Setup-Token" + +// tokenByteLength is the number of random bytes before hex-encoding (256 bits). +const tokenByteLength = 32 + +var errInvalidSetupToken = errors.New("invalid or missing setup token") + +// Generate returns a cryptographically random, hex-encoded setup token. +func Generate() (string, error) { + b := make([]byte, tokenByteLength) + if _, err := rand.Read(b); err != nil { + return "", err + } + + return hex.EncodeToString(b), nil +} + +// Validate checks that the request carries the expected setup token in the +// X-Setup-Token header. When expected is empty the gate is disabled and the +// request is always allowed. Comparison is constant-time. +func Validate(r *http.Request, expected string) *httperror.HandlerError { + if expected == "" { + return nil + } + + provided := r.Header.Get(HeaderName) + if subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 { + return httperror.Forbidden("Invalid or missing setup token. Provide the X-Setup-Token header with the token printed in the server logs at startup.", errInvalidSetupToken) + } + + return nil +} diff --git a/api/http/security/setuptoken/setuptoken_test.go b/api/http/security/setuptoken/setuptoken_test.go new file mode 100644 index 000000000..589fa327b --- /dev/null +++ b/api/http/security/setuptoken/setuptoken_test.go @@ -0,0 +1,46 @@ +package setuptoken + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Generate(t *testing.T) { + token, err := Generate() + require.NoError(t, err) + assert.Len(t, token, 64, "hex-encoded 32 bytes should be 64 characters") + + token2, err := Generate() + require.NoError(t, err) + assert.NotEqual(t, token, token2, "two generated tokens should differ") +} + +func Test_Validate_alwaysPassesWhenExpectedEmpty(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/", nil) + assert.Nil(t, Validate(r, "")) +} + +func Test_Validate_missingHeader(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/", nil) + herr := Validate(r, "secret") + require.NotNil(t, herr) + assert.Equal(t, http.StatusForbidden, herr.StatusCode) +} + +func Test_Validate_wrongToken(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/", nil) + r.Header.Set(HeaderName, "wrong") + herr := Validate(r, "secret") + require.NotNil(t, herr) + assert.Equal(t, http.StatusForbidden, herr.StatusCode) +} + +func Test_Validate_correctToken(t *testing.T) { + r := httptest.NewRequest(http.MethodPost, "/", nil) + r.Header.Set(HeaderName, "secret") + assert.Nil(t, Validate(r, "secret")) +} diff --git a/api/http/server.go b/api/http/server.go index 4f960a7e8..c56d1cd33 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -113,6 +113,7 @@ type Server struct { PlatformService platform.Service PullLimitCheckDisabled bool TrustedOrigins []string + SetupToken string } // Start starts the HTTP server @@ -149,6 +150,7 @@ func (server *Server) Start(ctx context.Context) error { server.ShutdownTrigger, adminMonitor, ) + backupHandler.SetupToken = server.SetupToken var roleHandler = roles.NewHandler(requestBouncer) roleHandler.DataStore = server.DataStore @@ -235,6 +237,7 @@ func (server *Server) Start(ctx context.Context) error { settingsHandler.JWTService = server.JWTService settingsHandler.LDAPService = server.LDAPService settingsHandler.SnapshotService = server.SnapshotService + settingsHandler.SetupTokenRequired = server.SetupToken != "" var sslHandler = sslhandler.NewHandler(requestBouncer) sslHandler.SSLService = server.SSLService @@ -282,6 +285,7 @@ func (server *Server) Start(ctx context.Context) error { userHandler.CryptoService = server.CryptoService userHandler.AdminCreationDone = server.AdminCreationDone userHandler.FileService = server.FileService + userHandler.SetupToken = server.SetupToken var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer) websocketHandler.DataStore = server.DataStore diff --git a/api/internal/registryutils/ecr_reg_token.go b/api/internal/registryutils/ecr_reg_token.go index 107204d29..a4387a8e7 100644 --- a/api/internal/registryutils/ecr_reg_token.go +++ b/api/internal/registryutils/ecr_reg_token.go @@ -38,9 +38,9 @@ func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) er return tx.Registry().Update(registry.ID, registry) } -// ValidateRegistriesECRTokens refreshes and persists ECR tokens for all registries that need it. +// RefreshAndPersistECRTokens refreshes and persists ECR tokens for all registries that need it. // Must be called with a real DataStoreTx (not a top-level DataStore) to avoid write-lock contention. -func ValidateRegistriesECRTokens(tx dataservices.DataStoreTx, registries []portainer.Registry) error { +func RefreshAndPersistECRTokens(tx dataservices.DataStoreTx, registries []portainer.Registry) { for i := range registries { reg := ®istries[i] if reg.Type != portainer.EcrRegistry { @@ -50,11 +50,12 @@ func ValidateRegistriesECRTokens(tx dataservices.DataStoreTx, registries []porta continue } if err := doGetRegToken(tx, reg); err != nil { - return fmt.Errorf("ECR registry %q credentials are invalid or expired. Error: %w", reg.Name, err) + log.Warn(). + Err(err). + Str("RegistryName", reg.Name). + Msg("Failed to get valid ECR registry token. Skip logging with this registry.") } } - - return nil } func EnsureRegTokenValid(tx dataservices.DataStoreTx, registry *portainer.Registry) error { diff --git a/api/internal/registryutils/ecr_reg_token_test.go b/api/internal/registryutils/ecr_reg_token_test.go index 70a8ce1b8..2c9b9a303 100644 --- a/api/internal/registryutils/ecr_reg_token_test.go +++ b/api/internal/registryutils/ecr_reg_token_test.go @@ -1,6 +1,8 @@ package registryutils_test import ( + "io" + "strings" "testing" "time" @@ -8,6 +10,7 @@ import ( "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/internal/registryutils" + zerolog "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" ) @@ -24,75 +27,40 @@ func newECRRegistry(id portainer.RegistryID, accessToken string, expiry int64) p } } -func TestValidateRegistriesECRTokens(t *testing.T) { - t.Parallel() - - t.Run("skips non-ECR registries without error", func(t *testing.T) { - t.Parallel() - _, ds := datastore.MustNewTestStore(t, true, false) - registries := []portainer.Registry{ - {ID: 1, Type: portainer.DockerHubRegistry, Name: "dockerhub"}, - {ID: 2, Type: portainer.CustomRegistry, Name: "custom"}, - } - require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - return registryutils.ValidateRegistriesECRTokens(tx, registries) - })) - }) - - t.Run("skips ECR registries with valid tokens", func(t *testing.T) { - t.Parallel() +func TestRefreshAndPersistECRTokens(t *testing.T) { + t.Run("does not modify token for ECR registry with valid token", func(t *testing.T) { _, ds := datastore.MustNewTestStore(t, true, false) reg := newECRRegistry(1, "valid-token", time.Now().Add(time.Hour).Unix()) require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - return registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{reg}) + registryutils.RefreshAndPersistECRTokens(tx, []portainer.Registry{reg}) + return nil })) + require.Equal(t, "valid-token", reg.AccessToken) }) - t.Run("returns nil for empty registry list", func(t *testing.T) { - t.Parallel() - _, ds := datastore.MustNewTestStore(t, true, false) - require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - return registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{}) - })) - }) + t.Run("does not block and leaves token empty when ECR token refresh fails", func(t *testing.T) { + var logOutput strings.Builder + setupLogOutput(t, &logOutput) - t.Run("returns error for ECR registry with missing token", func(t *testing.T) { - t.Parallel() _, ds := datastore.MustNewTestStore(t, true, false) reg := newECRRegistry(1, "", 0) require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - return tx.Registry().Create(®) - })) - - var validateErr error - _ = ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - validateErr = registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{reg}) + registryutils.RefreshAndPersistECRTokens(tx, []portainer.Registry{reg}) return nil - }) - require.Error(t, validateErr) - require.Contains(t, validateErr.Error(), "test-ecr") + })) + require.Empty(t, reg.AccessToken) + require.Contains(t, logOutput.String(), "test-ecr") + require.Contains(t, logOutput.String(), "Failed to get valid ECR registry token") }) +} - t.Run("stops on first invalid ECR registry and includes its name in error", func(t *testing.T) { - t.Parallel() - _, ds := datastore.MustNewTestStore(t, true, false) +func setupLogOutput(t *testing.T, w io.Writer) { + t.Helper() - validECR := newECRRegistry(1, "valid-token", time.Now().Add(time.Hour).Unix()) - invalidECR := newECRRegistry(2, "", 0) - invalidECR.Name = "invalid-ecr" - nonECR := portainer.Registry{ID: 3, Type: portainer.DockerHubRegistry} - - require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - return tx.Registry().Create(&invalidECR) - })) - - var validateErr error - _ = ds.UpdateTx(func(tx dataservices.DataStoreTx) error { - validateErr = registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{validECR, invalidECR, nonECR}) - return nil - }) - require.Error(t, validateErr) - require.Contains(t, validateErr.Error(), "invalid-ecr") + oldLogger := zerolog.Logger + zerolog.Logger = zerolog.Output(w) + t.Cleanup(func() { + zerolog.Logger = oldLogger }) } diff --git a/api/portainer.go b/api/portainer.go index aca2dc475..e30f1a7b6 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -111,6 +111,10 @@ type ( KubectlShellImageSet bool PullLimitCheckDisabled *bool TrustedOrigins *string + SSRFMode *string + SSRFAllowedHosts *[]string + NoSetupToken *bool + SetupToken *string } // CustomTemplateVariableDefinition @@ -150,6 +154,7 @@ type ( ResourceControl *ResourceControl `json:"ResourceControl"` Variables []CustomTemplateVariableDefinition GitConfig *gittypes.RepoConfig `json:"GitConfig"` + Artifact *Artifact `json:"artifact,omitempty"` // IsComposeFormat indicates if the Kubernetes template is created from a Docker Compose file IsComposeFormat bool `example:"false"` // EdgeTemplate indicates if this template purpose for Edge Stack @@ -558,8 +563,9 @@ type ( } PolicyChartSummary struct { - ChartName string `json:"ChartName"` - Fingerprint string `json:"Fingerprint"` + ChartName string `json:"ChartName"` + Fingerprint string `json:"Fingerprint"` + PolicyID PolicyID `json:"PolicyID,omitempty"` // 0 when server hasn't populated the field } PolicyChartStatus struct { @@ -581,17 +587,19 @@ type ( } PolicyChartBundle struct { - PolicyChartSummary `mapstructure:",squash"` - EncodedTgz string `json:"EncodedTgz"` - Namespace string `json:"Namespace"` - ReleaseName string `json:"ReleaseName,omitempty"` + PolicyChartSummary `mapstructure:",squash"` + EncodedTgz string `json:"EncodedTgz"` + Namespace string `json:"Namespace"` + ReleaseName string `json:"ReleaseName,omitempty"` + // Base64 YAML kubectl-applied by the agent before Helm install when set (e.g. Gatekeeper gatekeeper-system namespace + PSA labels). PreReleaseManifest string `json:"PreReleaseManifest,omitempty"` EncodedValues string `json:"EncodedValues"` PreInstallDeletions []ResourceDeletion `json:"PreInstallDeletions,omitempty"` PreInstallAdoptions []ResourceAdoption `json:"PreInstallAdoptions,omitempty"` + // WaitForCRDs lists CRD names that must be registered in API discovery after + // this chart installs before the agent proceeds to the next chart. + WaitForCRDs []string `json:"WaitForCRDs,omitempty"` // NoWait disables waiting for pods to be ready after install. - // Set to true for externally sourced charts whose startup timing cannot be controlled, - // to avoid leaving the release stuck in pending-install. NoWait bool `json:"NoWait,omitempty"` } @@ -621,6 +629,45 @@ type ( PolicyID int + // PolicyDesiredState is the per-policy desired state sent from server to agent + // in PollStatusResponse.PolicyStates (per-policy payload format). + PolicyDesiredState struct { + PolicyID PolicyID `json:"policyID"` + Type string `json:"type"` // e.g. "helm-k8s" + Fingerprint string `json:"fingerprint"` // install-affecting only; restore manifest excluded + Config []byte `json:"config"` // handler-specific config blob (e.g. HelmPolicyConfig JSON) + } + + // PolicyStatesAsyncPayload is the value of an async "policyStates" command. + // States carries per-policy desired state; ChartBundles carries helm chart tarballs + // for policies that need installing (emitted only on mutation, never idle polls). + PolicyStatesAsyncPayload struct { + States []PolicyDesiredState `json:"states"` + ChartBundles []PolicyChartBundle `json:"chartBundles,omitempty"` + RestoreBundle RestoreSettingsBundle `json:"restoreBundle,omitempty"` + } + + // PolicyActualState is the per-policy actual state reported by the agent + // via PUT /endpoints/{id}/edge/policies/statuses. + PolicyActualState struct { + PolicyID PolicyID `json:"policyID"` + Type string `json:"type"` + Fingerprint string `json:"fingerprint"` + Status string `json:"status"` // applying|applied|failed|removing + Message string `json:"message,omitempty"` + } + + // HelmPolicyConfig is the Config payload for "helm-k8s" PolicyDesiredState entries. + // Bundles are not included in the poll response Config — they travel separately + // (sync: on-demand GetCharts; async: PolicyStatesCommandPayload.ChartBundles). + // RestoreSettings is helm-internal metadata and is intentionally NOT part of the + // fingerprint — see Fingerprint contract in reconcile-refactor-plan.md. + HelmPolicyConfig struct { + Charts []PolicyChartSummary `json:"charts"` + Bundles []PolicyChartBundle `json:"bundles,omitempty"` + RestoreSettings *RestoreSettings `json:"restoreSettings,omitempty"` + } + // PolicyType represents the type of policy PolicyType string ) @@ -1254,13 +1301,13 @@ type ( // Source represents a GitOps source that can be referenced by stacks or deployments. Source struct { - ID SourceID `json:"id" example:"1"` - Name string `json:"name" example:"my-source"` - LastSync int64 `json:"lastSync,omitempty" example:"1587399600"` - Type SourceType `json:"type" example:"1"` - GitConfig *gittypes.RepoConfig `json:"gitConfig,omitempty"` - Registry *Registry `json:"registry,omitempty"` - HelmConfig *HelmConfig `json:"helmConfig,omitempty"` + ID SourceID `json:"id" example:"1"` + Name string `json:"name" example:"my-source"` + LastSync int64 `json:"lastSync,omitempty" example:"1587399600"` + Type SourceType `json:"type" example:"1"` + Git *gittypes.RepoConfig `json:"git,omitempty"` + Registry *Registry `json:"registry,omitempty"` + Helm *HelmConfig `json:"helm,omitempty"` } // SourceID represents a source identifier @@ -1489,11 +1536,11 @@ type ( // User represents a user account User struct { // User Identifier - ID UserID `json:"Id" example:"1"` - Username string `json:"Username" example:"bob"` + ID UserID `json:"Id" example:"1" validate:"required"` + Username string `json:"Username" example:"bob" validate:"required"` Password string `json:"Password,omitempty" swaggerignore:"true"` // User role (1 for administrator account and 2 for regular account) - Role UserRole `json:"Role" example:"1"` + Role UserRole `json:"Role" example:"1" validate:"required"` TokenIssueAt int64 `json:"TokenIssueAt" example:"1"` ThemeSettings UserThemeSettings `json:"ThemeSettings"` UseCache bool `json:"UseCache" example:"true"` @@ -1501,11 +1548,11 @@ type ( // Deprecated fields // Deprecated - UserTheme string `json:"UserTheme,omitempty" example:"dark"` + UserTheme string `json:"UserTheme,omitempty" example:"dark" swaggerignore:"true"` // Deprecated in DBVersion == 25 - PortainerAuthorizations Authorizations + PortainerAuthorizations Authorizations `swaggerignore:"true"` // Deprecated in DBVersion == 25 - EndpointAuthorizations EndpointAuthorizations + EndpointAuthorizations EndpointAuthorizations `swaggerignore:"true"` } // UserAccessPolicies represent the association of an access policy and a user @@ -1527,7 +1574,7 @@ type ( // UserThemeSettings represents the theme settings for a user UserThemeSettings struct { // Color represents the color theme of the UI - Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto"` + Color string `json:"color" example:"dark" enums:"dark,light,highcontrast,auto,"` } // Webhook represents a url webhook that can be used to update a service @@ -1548,26 +1595,29 @@ type ( // WebhookType represents the type of resource a webhook is related to WebhookType int - // Artifact represents a GitOps artifact produced by a source + // Artifact is one entry in a Workflow's artifact list, pairing target IDs with source files Artifact struct { - StackID StackID `json:"stackId,omitempty"` - EdgeStackID EdgeStackID `json:"edgeStackId,omitempty"` - ReferenceName string `json:"referenceName,omitempty" example:"refs/heads/main"` - ConfigFilePath string `json:"configFilePath,omitempty" example:"portainer.yaml"` - ConfigHash string `json:"configHash,omitempty" example:"abc123"` + StackID StackID `json:"stackId,omitempty"` + EdgeStackID EdgeStackID `json:"edgeStackId,omitempty"` + Files []ArtifactFile `json:"files,omitempty"` + EnvIDs []EndpointID `json:"envIds,omitempty"` + EnvGroups []EndpointGroupID `json:"envGroups,omitempty"` + EdgeGroups []EdgeGroupID `json:"edgeGroups,omitempty"` } - // ArtifactSources is one entry in a Workflow's ordered artifact-to-sources mapping - ArtifactSources struct { - Artifact Artifact `json:"artifact"` - SourceIDs []SourceID `json:"sourceIds,omitempty"` + // ArtifactFile represents one file within an artifact, tied to a specific source and location within it + ArtifactFile struct { + SourceID SourceID `json:"sourceId"` + Path string `json:"path,omitempty" example:"portainer.yaml"` + Ref string `json:"ref,omitempty" example:"refs/heads/main"` + Hash string `json:"hash,omitempty" example:"abc123"` } // Workflow represents a GitOps workflow Workflow struct { - ID WorkflowID `json:"id" example:"1"` - Name string `json:"name,omitempty" example:"my-workflow"` - Artifacts []ArtifactSources `json:"artifacts,omitempty"` + ID WorkflowID `json:"id" example:"1"` + Name string `json:"name,omitempty" example:"my-workflow"` + Artifacts []Artifact `json:"artifacts,omitempty"` } WorkflowID int @@ -2051,6 +2101,10 @@ const ( CSPEnvVar = "CSP" // CompactDBEnvVar is the environment variable used to enable/disable the startup compaction of the database CompactDBEnvVar = "COMPACT_DB" + // NoSetupTokenEnvVar is the environment variable used to disable the setup token requirement on an uninitialized instance + NoSetupTokenEnvVar = "PORTAINER_NO_SETUP_TOKEN" + // SetupTokenEnvVar is the environment variable used to provide a custom setup token for admin initialization and restore on an uninitialized instance + SetupTokenEnvVar = "PORTAINER_SETUP_TOKEN" ) // List of supported features diff --git a/api/stacks/deployments/deploy.go b/api/stacks/deployments/deploy.go index 9e54fcc74..76a0b7ed7 100644 --- a/api/stacks/deployments/deploy.go +++ b/api/stacks/deployments/deploy.go @@ -121,7 +121,7 @@ func redeployWhenChangedSecondStage( user *portainer.User, endpoint *portainer.Endpoint, ) error { - gitSrc, artifact, err := workflows.GitSourceAndArtifactForStack(datastore, stack.WorkflowID, stack.ID) + gitSrc, file, err := workflows.GitSourceAndArtifactForStack(datastore, stack.WorkflowID, stack.ID) if err != nil { return errors.WithMessagef(err, "failed to load git config for stack %v", stack.ID) } @@ -130,7 +130,7 @@ func redeployWhenChangedSecondStage( return nil } - gitConfig := workflows.MergeSourceAndArtifact(gitSrc, artifact) + gitConfig := workflows.MergeSourceAndFile(gitSrc, file) var gitCommitChangedOrForceUpdate bool @@ -225,8 +225,8 @@ func redeployWhenChangedSecondStage( newHash := gitConfig.ConfigHash - return workflows.UpdateArtifactForStack(tx, stack.WorkflowID, stack.ID, func(a *portainer.Artifact) { - a.ConfigHash = newHash + return workflows.UpdateArtifactFileForStack(tx, stack.WorkflowID, stack.ID, gitSrc.ID, func(a *portainer.ArtifactFile) { + a.Hash = newHash }) }); err != nil { return errors.WithMessagef(err, "failed to update the stack %v", stack.ID) diff --git a/api/stacks/deployments/deploy_test.go b/api/stacks/deployments/deploy_test.go index 023742ce5..e0fda0080 100644 --- a/api/stacks/deployments/deploy_test.go +++ b/api/stacks/deployments/deploy_test.go @@ -200,7 +200,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) { src := &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "url", ReferenceName: "ref", ConfigHash: "oldHash", @@ -209,7 +209,7 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) { err = store.Source().Create(src) require.NoError(t, err, "failed to create source") - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{src.ID}}}} + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}} err = store.Workflow().Create(wf) require.NoError(t, err, "failed to create workflow") @@ -247,7 +247,7 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) { src := &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "url", ReferenceName: "ref", ConfigHash: "oldHash", @@ -256,9 +256,9 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) { err = store.Source().Create(src) require.NoError(t, err, "failed to create source") - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{StackID: 1}, - SourceIDs: []portainer.SourceID{src.ID}, + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{ + StackID: 1, + Files: []portainer.ArtifactFile{{SourceID: src.ID}}, }}} err = store.Workflow().Create(wf) require.NoError(t, err, "failed to create workflow") @@ -290,7 +290,7 @@ func setupRedeployStore(t *testing.T, stackType portainer.StackType) (dataservic src := &portainer.Source{ Type: portainer.SourceTypeGit, - GitConfig: &gittypes.RepoConfig{ + Git: &gittypes.RepoConfig{ URL: "url", ReferenceName: "ref", ConfigHash: "oldHash", @@ -299,7 +299,7 @@ func setupRedeployStore(t *testing.T, stackType portainer.StackType) (dataservic err = store.Source().Create(src) require.NoError(t, err, "failed to create source") - wf := &portainer.Workflow{Artifacts: []portainer.ArtifactSources{{SourceIDs: []portainer.SourceID{src.ID}}}} + wf := &portainer.Workflow{Artifacts: []portainer.Artifact{{Files: []portainer.ArtifactFile{{SourceID: src.ID}}}}} err = store.Workflow().Create(wf) require.NoError(t, err, "failed to create workflow") diff --git a/api/stacks/deployments/deployer_remote.go b/api/stacks/deployments/deployer_remote.go index 1360b7675..033932467 100644 --- a/api/stacks/deployments/deployer_remote.go +++ b/api/stacks/deployments/deployer_remote.go @@ -169,13 +169,13 @@ func (d *stackDeployer) StopRemoteSwarmStack(ctx context.Context, stack *portain // * gather deployment logs and bubble them up func (d *stackDeployer) remoteStack(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, operation StackRemoteOperation, opts unpackerCmdBuilderOptions) error { if stack.WorkflowID != 0 && opts.gitConfig == nil { - src, artifact, err := workflows.GitSourceAndArtifactForStack(d.dataStore, stack.WorkflowID, stack.ID) + src, file, err := workflows.GitSourceAndArtifactForStack(d.dataStore, stack.WorkflowID, stack.ID) if err != nil { return errors.Wrap(err, "failed to load git config for remote stack") } if src != nil { - opts.gitConfig = workflows.MergeSourceAndArtifact(src, artifact) + opts.gitConfig = workflows.MergeSourceAndFile(src, file) } } diff --git a/api/stacks/deployments/deployment_compose_config.go b/api/stacks/deployments/deployment_compose_config.go index 14f6b000f..a34865c60 100644 --- a/api/stacks/deployments/deployment_compose_config.go +++ b/api/stacks/deployments/deployment_compose_config.go @@ -40,9 +40,7 @@ func CreateComposeStackDeploymentConfigTx(tx dataservices.DataStoreTx, securityC filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID) - if err := registryutils.ValidateRegistriesECRTokens(tx, filteredRegistries); err != nil { - return nil, err - } + registryutils.RefreshAndPersistECRTokens(tx, filteredRegistries) config := &ComposeStackDeploymentConfig{ stack: stack, @@ -73,6 +71,10 @@ func (config *ComposeStackDeploymentConfig) Deploy(ctx context.Context) error { } } + if err := stackutils.ValidateComposeURLs(ctx, config.stack, config.FileService); err != nil { + return err + } + if stackutils.IsRelativePathStack(config.stack) { return config.StackDeployer.DeployRemoteComposeStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.forcePullImage, config.ForceCreate) } diff --git a/api/stacks/deployments/deployment_swarm_config.go b/api/stacks/deployments/deployment_swarm_config.go index 8f70d0e2a..51d9a3f09 100644 --- a/api/stacks/deployments/deployment_swarm_config.go +++ b/api/stacks/deployments/deployment_swarm_config.go @@ -38,9 +38,7 @@ func CreateSwarmStackDeploymentConfigTx(tx dataservices.DataStoreTx, securityCon filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID) - if err := registryutils.ValidateRegistriesECRTokens(tx, filteredRegistries); err != nil { - return nil, err - } + registryutils.RefreshAndPersistECRTokens(tx, filteredRegistries) config := &SwarmStackDeploymentConfig{ stack: stack, @@ -73,6 +71,10 @@ func (config *SwarmStackDeploymentConfig) Deploy(ctx context.Context) error { } } + if err := stackutils.ValidateComposeURLs(ctx, config.stack, config.FileService); err != nil { + return err + } + if stackutils.IsRelativePathStack(config.stack) { return config.StackDeployer.DeployRemoteSwarmStack(ctx, config.stack, config.endpoint, config.registries, config.prune, config.pullImage) } diff --git a/api/stacks/stackbuilders/stack_git_builder.go b/api/stacks/stackbuilders/stack_git_builder.go index 4e242f8f1..573304012 100644 --- a/api/stacks/stackbuilders/stack_git_builder.go +++ b/api/stacks/stackbuilders/stack_git_builder.go @@ -74,9 +74,9 @@ func (b *GitMethodStackBuilder) prepare(ctx context.Context, payload *StackPaylo repoConfig.URL = gittypes.SanitizeURL(repoConfig.URL) src, err := workflows.FindOrCreateGitSource(tx, &portainer.Source{ - Name: gittypes.RepoName(repoConfig.URL), - Type: portainer.SourceTypeGit, - GitConfig: &repoConfig, + Name: gittypes.RepoName(repoConfig.URL), + Type: portainer.SourceTypeGit, + Git: &repoConfig, }) if err != nil { return fmt.Errorf("failed to find or create source: %w", err) @@ -84,14 +84,14 @@ func (b *GitMethodStackBuilder) prepare(ctx context.Context, payload *StackPaylo wf := &portainer.Workflow{ Name: b.stack.Name, - Artifacts: []portainer.ArtifactSources{{ - Artifact: portainer.Artifact{ - ReferenceName: repoConfig.ReferenceName, - ConfigFilePath: repoConfig.ConfigFilePath, - ConfigHash: repoConfig.ConfigHash, - StackID: b.stack.ID, - }, - SourceIDs: []portainer.SourceID{src.ID}, + Artifacts: []portainer.Artifact{{ + StackID: b.stack.ID, + Files: []portainer.ArtifactFile{{ + SourceID: src.ID, + Path: repoConfig.ConfigFilePath, + Ref: repoConfig.ReferenceName, + Hash: repoConfig.ConfigHash, + }}, }}, } if err := tx.Workflow().Create(wf); err != nil { diff --git a/api/stacks/stackutils/validation.go b/api/stacks/stackutils/validation.go index 75bb5015d..9f74ac149 100644 --- a/api/stacks/stackutils/validation.go +++ b/api/stacks/stackutils/validation.go @@ -2,10 +2,13 @@ package stackutils import ( "context" + "fmt" "path" + "strings" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/pkg/libhttp/ssrf" composeloader "github.com/compose-spec/compose-go/v2/loader" composetypes "github.com/compose-spec/compose-go/v2/types" @@ -68,6 +71,97 @@ func IsValidStackFile(config StackFileValidationConfig) error { return nil } +// ValidateComposeURLs parses each stack file and checks that every external URL +// (build contexts and image registry hostnames) is permitted by the active SSRF +// policy. It is a no-op when SSRF protection is disabled. +func ValidateComposeURLs(ctx context.Context, stack *portainer.Stack, fileService portainer.FileService) error { + if !ssrf.IsEnabled() { + return nil + } + + env := BuildEnvMap(stack) + workingDir := filesystem.JoinPaths(stack.ProjectPath, path.Dir(stack.EntryPoint)) + + for _, file := range GetStackFilePaths(stack, false) { + stackContent, err := fileService.GetFileContent(stack.ProjectPath, file) + if err != nil { + return errors.Wrap(err, "failed to get stack file content") + } + + if err := checkComposeFileURLs(ctx, stackContent, env, workingDir); err != nil { + return errors.Wrap(err, "stack file contains a URL blocked by the SSRF policy") + } + } + + return nil +} + +// ValidateEdgeStackComposeContent checks that every external URL in an edge +// stack's Compose file is permitted by the active SSRF policy. It is a no-op +// when SSRF protection is disabled or the deployment type is not compose. +func ValidateEdgeStackComposeContent(ctx context.Context, deploymentType portainer.EdgeStackDeploymentType, content []byte) error { + if !ssrf.IsEnabled() || deploymentType != portainer.EdgeStackDeploymentCompose { + return nil + } + + if err := checkComposeFileURLs(ctx, content, nil, ""); err != nil { + return errors.Wrap(err, "stack file contains a URL blocked by the SSRF policy") + } + + return nil +} + +func checkComposeFileURLs(ctx context.Context, content []byte, env map[string]string, workingDir string) error { + composeConfigDetails := composetypes.ConfigDetails{ + ConfigFiles: []composetypes.ConfigFile{{Content: content}}, + Environment: env, + WorkingDir: workingDir, + } + + composeConfig, err := composeloader.LoadWithContext(ctx, composeConfigDetails, composeloader.WithSkipValidation) + if err != nil { + return err + } + + for _, service := range composeConfig.Services { + if service.Build != nil { + buildCtx := service.Build.Context + if strings.HasPrefix(buildCtx, "http://") || strings.HasPrefix(buildCtx, "https://") { + if err := ssrf.CheckURL(ctx, buildCtx); err != nil { + return fmt.Errorf("service %q: build context URL blocked: %w", service.Name, err) + } + } + } + + if service.Image != "" { + if registry := extractImageRegistry(service.Image); registry != "" { + if err := ssrf.CheckURL(ctx, "https://"+registry); err != nil { + return fmt.Errorf("service %q: image registry %q blocked: %w", service.Name, registry, err) + } + } + } + } + + return nil +} + +// extractImageRegistry returns the registry hostname from an OCI image reference, +// or an empty string if the image resolves to Docker Hub (no explicit registry). +func extractImageRegistry(imageRef string) string { + ref, _, _ := strings.Cut(imageRef, "@") + + first, _, hasSlash := strings.Cut(ref, "/") + if !hasSlash { + return "" + } + + if strings.ContainsAny(first, ".:") || first == "localhost" { + return first + } + + return "" +} + func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error { env := BuildEnvMap(stack) workingDir := filesystem.JoinPaths(stack.ProjectPath, path.Dir(stack.EntryPoint)) diff --git a/api/stacks/stackutils/validation_test.go b/api/stacks/stackutils/validation_test.go index 2aca9357b..893f3925b 100644 --- a/api/stacks/stackutils/validation_test.go +++ b/api/stacks/stackutils/validation_test.go @@ -6,6 +6,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/stretchr/testify/require" ) @@ -271,3 +272,170 @@ services: err := ValidateStackFiles(stack, securitySettings, fileService) require.ErrorContains(t, err, "bind-mount disabled for non administrator users") } + +func TestExtractImageRegistry(t *testing.T) { + t.Parallel() + + cases := []struct { + image string + expected string + }{ + {"nginx", ""}, + {"nginx:latest", ""}, + {"library/nginx", ""}, + {"ghcr.io/owner/image:tag", "ghcr.io"}, + {"myregistry.com/image:tag", "myregistry.com"}, + {"myregistry.com:5000/image:tag", "myregistry.com:5000"}, + {"localhost/image:tag", "localhost"}, + {"localhost:5000/image:tag", "localhost:5000"}, + {"myregistry.com/image@sha256:abc", "myregistry.com"}, + {"169.254.169.254/image:tag", "169.254.169.254"}, + } + + for _, tc := range cases { + got := extractImageRegistry(tc.image) + require.Equal(t, tc.expected, got, "image: %s", tc.image) + } +} + +func TestValidateComposeURLs_DisabledSSRF(t *testing.T) { + ssrf.Configure(ssrf.Policy{}) + + stack := &portainer.Stack{ + ProjectPath: "/tmp/stack/1", + EntryPoint: "docker-compose.yml", + } + + fileService := mockFileService{ + fileContent: []byte(` +version: "3" +services: + web: + build: + context: http://169.254.169.254/repo.tar.gz +`), + projectVersionPath: "/tmp/stack/1", + } + + err := ValidateComposeURLs(t.Context(), stack, fileService) + require.NoError(t, err) +} + +func TestValidateComposeURLs_BuildContextBlocked(t *testing.T) { + ssrf.Configure(ssrf.Policy{Mode: ssrf.ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { ssrf.Configure(ssrf.Policy{}) }) + + stack := &portainer.Stack{ + ProjectPath: "/tmp/stack/1", + EntryPoint: "docker-compose.yml", + } + + fileService := mockFileService{ + fileContent: []byte(` +version: "3" +services: + web: + build: + context: http://169.254.169.254/repo.tar.gz + image: nginx +`), + projectVersionPath: "/tmp/stack/1", + } + + err := ValidateComposeURLs(t.Context(), stack, fileService) + require.ErrorContains(t, err, "SSRF policy") +} + +func TestValidateComposeURLs_BuildContextPath(t *testing.T) { + ssrf.Configure(ssrf.Policy{Mode: ssrf.ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { ssrf.Configure(ssrf.Policy{}) }) + + stack := &portainer.Stack{ + ProjectPath: "/tmp/stack/1", + EntryPoint: "docker-compose.yml", + } + + fileService := mockFileService{ + fileContent: []byte(` +version: "3" +services: + web: + build: + context: ./app + image: nginx +`), + projectVersionPath: "/tmp/stack/1", + } + + err := ValidateComposeURLs(t.Context(), stack, fileService) + require.NoError(t, err) +} + +func TestValidateComposeURLs_ImageRegistryBlocked(t *testing.T) { + ssrf.Configure(ssrf.Policy{Mode: ssrf.ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { ssrf.Configure(ssrf.Policy{}) }) + + stack := &portainer.Stack{ + ProjectPath: "/tmp/stack/1", + EntryPoint: "docker-compose.yml", + } + + fileService := mockFileService{ + fileContent: []byte(` +version: "3" +services: + web: + image: 169.254.169.254/myimage:latest +`), + projectVersionPath: "/tmp/stack/1", + } + + err := ValidateComposeURLs(t.Context(), stack, fileService) + require.ErrorContains(t, err, "SSRF policy") +} + +func TestValidateComposeURLs_ImageRegistryAllowed(t *testing.T) { + ssrf.Configure(ssrf.Policy{Mode: ssrf.ModeEnforce, AllowedHosts: []string{"myregistry.com"}}) + t.Cleanup(func() { ssrf.Configure(ssrf.Policy{}) }) + + stack := &portainer.Stack{ + ProjectPath: "/tmp/stack/1", + EntryPoint: "docker-compose.yml", + } + + fileService := mockFileService{ + fileContent: []byte(` +version: "3" +services: + web: + image: myregistry.com/myimage:latest +`), + projectVersionPath: "/tmp/stack/1", + } + + err := ValidateComposeURLs(t.Context(), stack, fileService) + require.NoError(t, err) +} + +func TestValidateComposeURLs_DockerHubImageAllowed(t *testing.T) { + ssrf.Configure(ssrf.Policy{Mode: ssrf.ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { ssrf.Configure(ssrf.Policy{}) }) + + stack := &portainer.Stack{ + ProjectPath: "/tmp/stack/1", + EntryPoint: "docker-compose.yml", + } + + fileService := mockFileService{ + fileContent: []byte(` +version: "3" +services: + web: + image: nginx:latest +`), + projectVersionPath: "/tmp/stack/1", + } + + err := ValidateComposeURLs(t.Context(), stack, fileService) + require.NoError(t, err) +} diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 6a65df7ef..6795e149e 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -32,6 +32,7 @@ export function PublicSettingsViewModel(settings) { this.Features = settings.Features; this.Edge = new EdgeSettingsViewModel(settings.Edge); this.DefaultRegistry = settings.DefaultRegistry; + this.RequiresSetupToken = settings.RequiresSetupToken; } export function InternalAuthSettingsViewModel(data) { diff --git a/app/portainer/services/api/backupService.js b/app/portainer/services/api/backupService.js index ca1637ebf..026328ac5 100644 --- a/app/portainer/services/api/backupService.js +++ b/app/portainer/services/api/backupService.js @@ -11,8 +11,8 @@ angular.module('portainer.app').factory('BackupService', [ return Backup.download({}, payload).$promise; }; - service.uploadBackup = function (file, password) { - return FileUploadService.uploadBackup(file, password); + service.uploadBackup = function (file, password, setupToken) { + return FileUploadService.uploadBackup(file, password, setupToken); }; service.getS3Settings = function () { diff --git a/app/portainer/services/api/userService.js b/app/portainer/services/api/userService.js index 1ee22a7b5..2d1102f8a 100644 --- a/app/portainer/services/api/userService.js +++ b/app/portainer/services/api/userService.js @@ -2,6 +2,7 @@ import _ from 'lodash-es'; import { UserViewModel } from '@/portainer/models/user'; import { getUsers } from '@/portainer/users/user.service'; import { getUser } from '@/portainer/users/queries/useUser'; +import { userAdminInit } from '@api/sdk.gen'; import { TeamMembershipModel } from '../../models/teamMembership'; @@ -84,8 +85,11 @@ export function UserService($q, Users, TeamService) { return deferred.promise; }; - service.initAdministrator = function (username, password) { - return Users.initAdminUser({ Username: username, Password: password }).$promise; + service.initAdministrator = function (username, password, setupToken) { + return userAdminInit({ + body: { Username: username, Password: password }, + ...(setupToken && { headers: { 'X-Setup-Token': setupToken } }), + }); }; service.administratorExists = function () { diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 877cdf687..c0f9e824d 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -38,13 +38,11 @@ function FileUploadFactory($q, Upload) { }); }; - service.uploadBackup = function (file, password) { + service.uploadBackup = function (file, password, setupToken) { return Upload.upload({ url: 'api/restore', - data: { - file, - password, - }, + data: { file, password }, + headers: setupToken ? { 'X-Setup-Token': setupToken } : {}, }); }; diff --git a/app/portainer/views/init/admin/initAdmin.html b/app/portainer/views/init/admin/initAdmin.html index 4733ca216..e05f9d33b 100644 --- a/app/portainer/views/init/admin/initAdmin.html +++ b/app/portainer/views/init/admin/initAdmin.html @@ -65,6 +65,17 @@ + +
+ +
+ +
+
+
@@ -82,7 +93,7 @@
+ +
+ +
+ +
+
+
- } - /> -
- Sort: {sortBy} | Platform:{' '} - {platformFilter ?? 'All'} | Governance:{' '} - {govFilter ?? 'All'} -
-
- ); -} - -export function MultipleBadgesPersist() { - const [sortBy, setSortBy] = useState('Governance'); - const [searchTerm, setSearchTerm] = useState(''); - - const sortOptions: SortOption[] = [ - { key: 'Name', label: 'Name' }, - { - key: 'Platform', - label: 'Platform', - dropdown: { - options: [{ key: 'Docker', count: 5 }], - selected: 'Docker', - onSelect: () => {}, - }, - }, - { - key: 'Governance', - label: 'Governance', - dropdown: { - options: [{ key: 'Ungoverned', count: 2 }], - selected: 'Ungoverned', - onSelect: () => {}, - }, - }, - ]; - - return ( - - ); -} - -MultipleBadgesPersist.play = async ({ - canvasElement, -}: { - canvasElement: HTMLElement; -}) => { - const canvas = within(canvasElement); - - // Governance is the active sort — its badge should show - const govBtn = canvas.getByRole('button', { name: /Governance/i }); - await expect(govBtn).toHaveTextContent('Ungoverned'); - - // Platform is inactive — its badge should ALSO show - const platformBtn = canvas.getByRole('button', { name: /Platform/i }); - await expect(platformBtn).toHaveTextContent('Docker'); -}; diff --git a/app/react/components/GroupSortTable/GroupSortTableHeader.test.tsx b/app/react/components/GroupSortTable/GroupSortTableHeader.test.tsx deleted file mode 100644 index aef591e1d..000000000 --- a/app/react/components/GroupSortTable/GroupSortTableHeader.test.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { GroupSortTableHeader, SortOption } from './GroupSortTableHeader'; - -const defaultGroups = [ - { key: 'Docker', count: 3 }, - { key: 'Kubernetes', count: 2 }, -]; - -const defaultSortOptions: SortOption[] = [ - { key: 'Name', label: 'Name' }, - { - key: 'Platform', - label: 'Platform', - dropdown: { - options: defaultGroups, - selected: null, - onSelect: vi.fn(), - }, - }, - { key: 'Health', label: 'Health' }, -]; - -function renderHeader( - overrides: Partial< - React.ComponentProps> - > = {} -) { - const props = { - sortBy: 'Name' as string, - onSortChange: vi.fn(), - searchTerm: '', - onSearchChange: vi.fn(), - sortOptions: defaultSortOptions, - ...overrides, - }; - - return { - ...render(), - props, - }; -} - -describe('GroupSortTableHeader', () => { - test('renders all sort option buttons', () => { - renderHeader(); - - expect(screen.getByRole('button', { name: /Name/i })).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /Platform/i }) - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Health/i })).toBeInTheDocument(); - }); - - test('plain button calls onSortChange when clicked', async () => { - const user = userEvent.setup(); - const onSortChange = vi.fn(); - renderHeader({ sortBy: 'Platform', onSortChange }); - - await user.click(screen.getByRole('button', { name: /^Name$/i })); - - expect(onSortChange).toHaveBeenCalledWith('Name'); - }); - - test('dropdown button opens menu with options', async () => { - const user = userEvent.setup(); - renderHeader({ sortBy: 'Platform' }); - - await user.click(screen.getByRole('button', { name: /Platform/i })); - - expect(screen.getByRole('menuitem', { name: /All/ })).toBeVisible(); - expect(screen.getByRole('menuitem', { name: /Docker/ })).toBeVisible(); - expect(screen.getByRole('menuitem', { name: /Kubernetes/ })).toBeVisible(); - }); - - test('clicking dropdown button does not change the active sort', async () => { - const user = userEvent.setup(); - const onSortChange = vi.fn(); - renderHeader({ sortBy: 'Name', onSortChange }); - - await user.click(screen.getByRole('button', { name: /Platform/i })); - - expect(onSortChange).not.toHaveBeenCalled(); - }); - - test('active dropdown filter is shown as a badge', () => { - const options: SortOption[] = [ - { - key: 'Platform', - label: 'Platform', - dropdown: { - options: defaultGroups, - selected: 'Docker', - onSelect: vi.fn(), - }, - }, - ]; - renderHeader({ sortBy: 'Platform', sortOptions: options }); - - const btn = screen.getByRole('button', { name: /Platform/i }); - expect(btn).toHaveTextContent('Docker'); - }); - - test('dropdown shows counts for each option', async () => { - const user = userEvent.setup(); - renderHeader({ sortBy: 'Platform' }); - - await user.click(screen.getByRole('button', { name: /Platform/i })); - - const menu = screen.getByRole('menu', { name: /Platform/i }); - expect(menu).toHaveTextContent('3'); - expect(menu).toHaveTextContent('2'); - }); - - test('search input renders with the correct placeholder', () => { - renderHeader(); - - expect(screen.getByPlaceholderText('Filter...')).toBeInTheDocument(); - }); - - test('renders custom search placeholder', () => { - renderHeader({ searchPlaceholder: 'Search environments...' }); - - expect( - screen.getByPlaceholderText('Search environments...') - ).toBeInTheDocument(); - }); - - test('renders action button when provided', () => { - renderHeader({ - actionButton: , - }); - - expect( - screen.getByRole('button', { name: /Add item/i }) - ).toBeInTheDocument(); - }); - - test('shows ascending arrow on active sort button when sortAsc is true', () => { - renderHeader({ sortBy: 'Name', sortAsc: true }); - - const activeBtn = screen.getByRole('button', { name: /^Name$/i }); - expect( - activeBtn.querySelector('.lucide-arrow-down-a-z') - ).toBeInTheDocument(); - expect( - activeBtn.querySelector('.lucide-arrow-down-z-a') - ).not.toBeInTheDocument(); - }); - - test('shows descending arrow on active sort button when sortAsc is false', () => { - renderHeader({ sortBy: 'Name', sortAsc: false }); - - const activeBtn = screen.getByRole('button', { name: /^Name$/i }); - expect( - activeBtn.querySelector('.lucide-arrow-down-z-a') - ).toBeInTheDocument(); - expect( - activeBtn.querySelector('.lucide-arrow-down-a-z') - ).not.toBeInTheDocument(); - }); - - test('defaults to ascending arrow on active sort button when sortAsc is undefined', () => { - renderHeader({ sortBy: 'Name' }); - - const activeBtn = screen.getByRole('button', { name: /^Name$/i }); - expect( - activeBtn.querySelector('.lucide-arrow-down-a-z') - ).toBeInTheDocument(); - expect( - activeBtn.querySelector('.lucide-arrow-down-z-a') - ).not.toBeInTheDocument(); - }); - - test('does not show sort direction arrow on inactive sort buttons', () => { - renderHeader({ sortBy: 'Name', sortAsc: true }); - - const inactiveBtn = screen.getByRole('button', { name: /^Health$/i }); - expect( - inactiveBtn.querySelector('.lucide-arrow-down-a-z') - ).not.toBeInTheDocument(); - expect( - inactiveBtn.querySelector('.lucide-arrow-down-z-a') - ).not.toBeInTheDocument(); - }); - - test('does not render clear button when onClear is not provided', () => { - renderHeader(); - - expect( - screen.queryByRole('button', { name: /Clear all sort options/i }) - ).not.toBeInTheDocument(); - }); - - test('renders clear button and calls onClear when clicked', async () => { - const user = userEvent.setup(); - const onClear = vi.fn(); - renderHeader({ onClear }); - - const clearBtn = screen.getByRole('button', { - name: /Clear all sort options/i, - }); - expect(clearBtn).toBeInTheDocument(); - - await user.click(clearBtn); - - expect(onClear).toHaveBeenCalledTimes(1); - }); -}); diff --git a/app/react/components/GroupSortTable/GroupSortTableHeader.tsx b/app/react/components/GroupSortTable/GroupSortTableHeader.tsx deleted file mode 100644 index c332c9526..000000000 --- a/app/react/components/GroupSortTable/GroupSortTableHeader.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react'; -import clsx from 'clsx'; -import { ArrowDownAZ, ArrowDownZA, X } from 'lucide-react'; - -import { SearchBar } from '@@/datatables/SearchBar'; -import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren'; - -import { DropdownMenu, DropdownOption } from '../DropdownMenu/DropdownMenu'; - -export interface SortOption { - key: TSortKey; - label: string; - dropdown?: { - options: DropdownOption[]; - selected: string | null; - onSelect: (value: string | null) => void; - }; -} - -interface Props { - sortBy: TSortKey; - sortAsc?: boolean; - onSortChange: (key: TSortKey) => void; - onClear?: () => void; - searchTerm: string; - onSearchChange: (term: string) => void; - sortOptions: SortOption[]; - searchPlaceholder?: string; - actionButton?: React.ReactNode; - searchDataCy?: string; -} - -const clearBtnClasses = - 'inline-flex items-center justify-center rounded-md border-0 bg-transparent p-1.5 transition-colors ' + - 'text-gray-8 hover:bg-gray-4 hover:text-gray-11 ' + - 'th-dark:text-gray-5 th-dark:hover:bg-gray-iron-9 th-dark:hover:text-white ' + - 'th-highcontrast:border th-highcontrast:border-solid th-highcontrast:border-white ' + - 'th-highcontrast:text-white th-highcontrast:hover:bg-white th-highcontrast:hover:text-black'; - -const baseBtn = - 'px-4 py-1.5 text-xs align-middle font-medium transition-colors'; -const activeBtn = - 'z-10 border-none rounded-md font-medium ' + - 'bg-white text-gray-8 ' + - 'th-dark:bg-gray-iron-10 th-dark:text-white ' + - 'th-highcontrast:bg-white th-highcontrast:text-black'; -const inactiveBtn = - 'text-muted border-none rounded-md ' + - 'bg-gray-4 hover:bg-gray-2 ' + - 'th-dark:bg-gray-iron-11 th-dark:text-white th-dark:hover:bg-gray-iron-9 ' + - 'th-highcontrast:bg-black th-highcontrast:text-white th-highcontrast:hover:bg-white th-highcontrast:hover:text-black'; - -export function GroupSortTableHeader({ - sortBy, - sortAsc, - onSortChange, - onClear, - searchTerm, - onSearchChange, - sortOptions, - searchPlaceholder = 'Filter...', - actionButton, - searchDataCy, -}: Props) { - return ( -
- - SORT BY: - -
- {sortOptions.map((option, index) => { - const isActive = sortBy === option.key; - const isFirst = index === 0; - const isLast = index === sortOptions.length - 1; - const cornerClasses = clsx( - isFirst && 'rounded-l-md', - isLast && 'rounded-r-md' - ); - const btnClasses = clsx( - baseBtn, - isActive ? activeBtn : inactiveBtn, - cornerClasses - ); - - if (option.dropdown) { - return ( - - ); - } - - return ( - - ); - })} -
- {onClear && ( - - - - )} -
- - {actionButton} -
-
- ); -} diff --git a/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx b/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx index f8851f1cc..58b80a915 100644 --- a/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx +++ b/app/react/components/ImageConfigFieldset/ImageConfigFieldset.test.tsx @@ -5,6 +5,7 @@ import { render, fireEvent } from '@testing-library/react'; import { http, server } from '@/setup-tests/server'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; +import { createMockEnvironment } from '@/react-tools/test-mocks'; import { ImageConfigFieldset } from './ImageConfigFieldset'; import { Values } from './types'; @@ -71,7 +72,9 @@ function renderComponent({ } = {}) { server.use( http.get('/api/registries/:id', () => HttpResponse.json({})), - http.get('/api/endpoints/:id', () => HttpResponse.json({})) + http.get('/api/endpoints/:id', () => + HttpResponse.json(createMockEnvironment()) + ) ); const Wrapped = withTestQueryProvider(ImageConfigFieldset); diff --git a/app/react/components/form-components/DisabledMultiValue.tsx b/app/react/components/form-components/DisabledMultiValue.tsx index 4513d2907..52e0f7a97 100644 --- a/app/react/components/form-components/DisabledMultiValue.tsx +++ b/app/react/components/form-components/DisabledMultiValue.tsx @@ -1,6 +1,10 @@ import type { ReactNode } from 'react'; -import { components, type GroupBase, type MultiValueProps } from 'react-select'; -import type { MultiValueRemoveProps } from 'react-select/dist/declarations/src/components/MultiValue'; +import { + components, + type GroupBase, + type MultiValueProps, + type MultiValueRemoveProps, +} from 'react-select'; import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren'; diff --git a/app/react/components/form-components/PortainerSelect.tsx b/app/react/components/form-components/PortainerSelect.tsx index 827b36d24..b1eb4d3fe 100644 --- a/app/react/components/form-components/PortainerSelect.tsx +++ b/app/react/components/form-components/PortainerSelect.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; import type { AriaAttributes } from 'react'; import { + FilterOptionOption, GroupBase, OptionsOrGroups, SelectComponentsConfig, } from 'react-select'; import _ from 'lodash'; -import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; import { AutomationTestingProps } from '@/types'; diff --git a/app/react/components/form-components/ReactSelect.tsx b/app/react/components/form-components/ReactSelect.tsx index 73d76ec6d..61ae684ab 100644 --- a/app/react/components/form-components/ReactSelect.tsx +++ b/app/react/components/form-components/ReactSelect.tsx @@ -11,10 +11,10 @@ import ReactSelect, { InputProps, OptionsOrGroups, Props as ReactSelectProps, + SelectInstance as ReactSelectType, } from 'react-select'; import clsx from 'clsx'; import { RefAttributes, useMemo, useCallback } from 'react'; -import ReactSelectType from 'react-select/dist/declarations/src/Select'; import './ReactSelect.css'; import { AutomationTestingProps } from '@/types'; diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.test.tsx b/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.test.tsx index 1b6fc1356..e0b69e7c4 100644 --- a/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.test.tsx +++ b/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.test.tsx @@ -8,7 +8,10 @@ import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withTestRouter } from '@/react/test-utils/withRouter'; import { server } from '@/setup-tests/server'; import { Role, User } from '@/portainer/users/types'; -import { createMockUsers } from '@/react-tools/test-mocks'; +import { + createMockEnvironment, + createMockUsers, +} from '@/react-tools/test-mocks'; import { ConfigsDatatable } from './ConfigsDatatable'; @@ -35,11 +38,13 @@ vi.mock('@@/Link', () => ({ beforeEach(() => { server.use( http.get('/api/endpoints/1', () => - HttpResponse.json({ - Id: 1, - Name: 'test-environment', - Type: 1, - }) + HttpResponse.json( + createMockEnvironment({ + Id: 1, + Name: 'test-environment', + Type: 1, + }) + ) ) ); }); diff --git a/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx b/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx index 7d1a3ae31..de6921f67 100644 --- a/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx +++ b/app/react/docker/containers/CreateView/ResourcesTab/GpuFieldset/GpuFieldset.tsx @@ -1,11 +1,12 @@ import { useMemo } from 'react'; -import { components, MultiValue } from 'react-select'; -import { MultiValueRemoveProps } from 'react-select/dist/declarations/src/components/MultiValue'; import { ActionMeta, + components, + MultiValue, + MultiValueRemoveProps, OnChangeValue, -} from 'react-select/dist/declarations/src/types'; -import { OptionProps } from 'react-select/dist/declarations/src/components/Option'; + OptionProps, +} from 'react-select'; import { Select } from '@@/form-components/ReactSelect'; import { Switch } from '@@/form-components/SwitchField/Switch'; diff --git a/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx b/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx index 6d6173833..571c2e203 100644 --- a/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx +++ b/app/react/docker/stacks/CreateView/CreateStackForm/CreateStackForm.webhook.test.tsx @@ -10,7 +10,10 @@ import { EnvironmentType } from '@/react/portainer/environments/types'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withTestRouter } from '@/react/test-utils/withRouter'; -import { createMockUsers } from '@/react-tools/test-mocks'; +import { + createMockEnvironment, + createMockUsers, +} from '@/react-tools/test-mocks'; import { Role } from '@/portainer/users/types'; import { mockFormValues } from './test-utils'; @@ -34,14 +37,13 @@ describe('CreateStackForm - Webhook ID Integration', () => { server.use( http.get('/api/endpoints/1', () => - HttpResponse.json({ - Id: 1, - Type: EnvironmentType.Docker, - ComposeSyntaxMaxVersion: '3', - ChangeWindow: { - Enabled: false, - }, - }) + HttpResponse.json( + createMockEnvironment({ + Id: 1, + Type: EnvironmentType.Docker, + ComposeSyntaxMaxVersion: '3', + }) + ) ) ); }); diff --git a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx index 7942670c1..ebcc9a367 100644 --- a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx +++ b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.test.tsx @@ -8,7 +8,11 @@ import { server } from '@/setup-tests/server'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { suppressConsoleLogs } from '@/setup-tests/suppress-console'; -import { createMockUsers, createMockStack } from '@/react-tools/test-mocks'; +import { + createMockUsers, + createMockStack, + createMockEnvironment, +} from '@/react-tools/test-mocks'; import { EnvironmentType } from '@/react/portainer/environments/types'; import { Role } from '@/portainer/users/types'; import { withTestRouter } from '@/react/test-utils/withRouter'; @@ -108,11 +112,13 @@ describe('initial loading', () => { server.use( http.get('/api/endpoints/:id', () => { envFetched = true; - return HttpResponse.json({ - Id: 1, - Name: 'local', - Type: EnvironmentType.Docker, - }); + return HttpResponse.json( + createMockEnvironment({ + Id: 1, + Name: 'local', + Type: EnvironmentType.Docker, + }) + ); }) ); @@ -402,11 +408,13 @@ function setupMswHandlers({ if (!shouldReturnEnv) { return HttpResponse.json(null, { status: 404 }); } - return HttpResponse.json({ - Id: 1, - Name: 'local', - Type: envType, - }); + return HttpResponse.json( + createMockEnvironment({ + Id: 1, + Name: 'local', + Type: envType, + }) + ); }), http.get('https://raw.githubusercontent.com/*', () => { if (!shouldReturnSchema) { diff --git a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx index 1447709c7..a370b0a87 100644 --- a/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx +++ b/app/react/docker/stacks/ItemView/StackEditorTab/StackEditorTab.webhook.test.tsx @@ -10,7 +10,11 @@ import { EnvironmentType } from '@/react/portainer/environments/types'; import { withTestQueryProvider } from '@/react/test-utils/withTestQuery'; import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withTestRouter } from '@/react/test-utils/withRouter'; -import { createMockStack, createMockUsers } from '@/react-tools/test-mocks'; +import { + createMockEnvironment, + createMockStack, + createMockUsers, +} from '@/react-tools/test-mocks'; import { Role } from '@/portainer/users/types'; import { suppressConsoleLogs } from '@/setup-tests/suppress-console'; @@ -39,14 +43,13 @@ describe('StackEditorTab - Webhook ID Handling', () => { server.use( http.get('/api/endpoints/1', () => - HttpResponse.json({ - Id: 1, - Type: EnvironmentType.Docker, - ComposeSyntaxMaxVersion: '3', - ChangeWindow: { - Enabled: false, - }, - }) + HttpResponse.json( + createMockEnvironment({ + Id: 1, + Type: EnvironmentType.Docker, + ComposeSyntaxMaxVersion: '3', + }) + ) ) ); }); diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.test.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.test.tsx index 28277c6f3..005056b04 100644 --- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.test.tsx +++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.test.tsx @@ -214,15 +214,15 @@ describe('ApplicationContainersDatatable', () => { expect(await screen.findByText('No items.')).toBeVisible(); }); - it('expands a pod row to reveal its containers', async () => { + it('collapses a pod row to hide its containers', async () => { const user = userEvent.setup(); renderComponent(); await screen.findByText('test-pod-1'); - await user.click(screen.getByRole('button', { name: 'Expand' })); + await user.click(screen.getByRole('button', { name: 'Collapse' })); - expect(await screen.findByText('nginx')).toBeVisible(); + expect(await screen.queryAllByText('nginx')).toStrictEqual([]); }); it('shows the Init badge for init containers', async () => { @@ -265,11 +265,9 @@ describe('ApplicationContainersDatatable', () => { ) ); - const user = userEvent.setup(); renderComponent(); await screen.findByText('test-pod-1'); - await user.click(screen.getByRole('button', { name: 'Expand' })); expect(await screen.findByText('Init')).toBeVisible(); }); @@ -310,11 +308,8 @@ describe('ApplicationContainersDatatable', () => { ) ); - const user = userEvent.setup(); renderComponent(); - await screen.findByText('test-pod-1'); - await user.click(screen.getByRole('button', { name: 'Expand' })); expect(await screen.findByText('Sidecar')).toBeVisible(); }); @@ -329,11 +324,9 @@ describe('ApplicationContainersDatatable', () => { ) ); - const user = userEvent.setup(); renderComponent(); await screen.findByText('test-pod-1'); - await user.click(screen.getByRole('button', { name: 'Expand' })); expect( await screen.findByTestId('application-container-stats-nginx') @@ -341,11 +334,9 @@ describe('ApplicationContainersDatatable', () => { }); it('hides the stats link when server metrics is disabled', async () => { - const user = userEvent.setup(); renderComponent(); await screen.findByText('test-pod-1'); - await user.click(screen.getByRole('button', { name: 'Expand' })); await screen.findByText('nginx'); expect( @@ -354,17 +345,25 @@ describe('ApplicationContainersDatatable', () => { }); it('shows the logs link for a container that has started', async () => { - const user = userEvent.setup(); renderComponent(); await screen.findByText('test-pod-1'); - await user.click(screen.getByRole('button', { name: 'Expand' })); expect( await screen.findByTestId('application-container-logs-nginx') ).toBeVisible(); }); + it('does not render a restart button', async () => { + renderComponent(); + + await screen.findByText('test-pod-1'); + + expect( + screen.queryByTestId('application-pod-restart-test-pod-1') + ).not.toBeInTheDocument(); + }); + it('calls the delete API after the user confirms deletion', async () => { let deleteRequested = false; server.use( diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx index 491ecac3e..eb03077cc 100644 --- a/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx +++ b/app/react/kubernetes/applications/DetailsView/ApplicationContainersDatatable/ApplicationContainersDatatable.tsx @@ -130,6 +130,10 @@ export function ApplicationContainersDatatable() { /> )} + initialTableState={{ + pagination: { pageIndex: 0, pageSize: 5 }, + expanded: true, + }} />
diff --git a/app/react/portainer/environments/ItemView/GeneralEnvironmentForm/GeneralEnvironmentForm.test.tsx b/app/react/portainer/environments/ItemView/GeneralEnvironmentForm/GeneralEnvironmentForm.test.tsx index c15cae867..e1f28829e 100644 --- a/app/react/portainer/environments/ItemView/GeneralEnvironmentForm/GeneralEnvironmentForm.test.tsx +++ b/app/react/portainer/environments/ItemView/GeneralEnvironmentForm/GeneralEnvironmentForm.test.tsx @@ -113,7 +113,7 @@ describe('GeneralEnvironmentForm', () => { setTimeout(resolve, 100); }); requestPayload = await request.json(); - return HttpResponse.json({}); + return HttpResponse.json(createMockEnvironment()); }) ); @@ -354,7 +354,7 @@ describe('GeneralEnvironmentForm', () => { setTimeout(resolve, 100); }); requestPayload = await request.json(); - return HttpResponse.json({}); + return HttpResponse.json(createMockEnvironment()); }) ); diff --git a/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx b/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx index c1aa4c0cc..e9a9d4e5f 100644 --- a/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx +++ b/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.test.tsx @@ -253,8 +253,11 @@ describe('EditGroupView', () => { // Wait for form to load await screen.findByLabelText(/Name/i); - // The Environments tab should be visible - expect(screen.getByText('Environments')).toBeVisible(); + // The Environments tab should be visible — target by data-cy so we + // don't collide with the "Environments" stat block label in the header. + const environmentsTab = screen.getByTestId('tab-0'); + expect(environmentsTab).toBeVisible(); + expect(environmentsTab).toHaveTextContent(/Environments/i); }); }); @@ -492,6 +495,56 @@ describe('EditGroupView', () => { }); }); + describe('API request - size parameter', () => { + it('should request the group with size=true so TypeInfo is populated', async () => { + let capturedUrl: string | undefined; + + server.use( + http.get('/api/endpoint_groups/2', ({ request }) => { + capturedUrl = request.url; + return HttpResponse.json(mockGroup); + }), + http.get('/api/tags', () => HttpResponse.json([])), + http.get('/api/endpoints', () => + HttpResponse.json([], { + headers: { + 'x-total-count': '0', + 'x-total-available': '0', + }, + }) + ) + ); + + const Wrapped = withTestQueryProvider( + withTestRouter(withUserProvider(EditGroupView)) + ); + render(); + + await screen.findByLabelText(/Name/i); + + // size=true is required for TypeInfo (Docker/Kubernetes/Podman counts) to be + // included in the response. Without it, EnvironmentTypeBreakdown in GroupHeader + // shows nothing. + expect(new URL(capturedUrl!).searchParams.get('size')).toBe('true'); + }); + + it('should show per-platform breakdown in GroupHeader when TypeInfo is returned', async () => { + const groupWithTypeInfo = createMockEnvironmentGroup({ + Id: 2, + Name: 'Test Group', + Description: 'Test description', + TagIds: [1], + Total: 3, + TypeInfo: { Docker: 2, Kubernetes: 1, Podman: 0, Mixed: true }, + }); + + renderEditGroupView({ groupData: groupWithTypeInfo }); + + expect(await screen.findByText('Docker')).toBeVisible(); + expect(screen.getByText('Kubernetes')).toBeVisible(); + }); + }); + describe('Delete flow', () => { it('should render the delete button', async () => { renderEditGroupView(); diff --git a/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.tsx b/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.tsx index f285bc0b2..24fba3ce1 100644 --- a/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.tsx +++ b/app/react/portainer/environments/environment-groups/ItemView/EditGroupView.tsx @@ -23,7 +23,8 @@ export function EditGroupView() { const deleteMutation = useDeleteEnvironmentGroupMutation(); const [addEnvsDrawerOpen, setAddEnvsDrawerOpen] = useState(false); const groupQuery = useGroup( - deleteMutation.isLoading || deleteMutation.isSuccess ? undefined : groupId + deleteMutation.isLoading || deleteMutation.isSuccess ? undefined : groupId, + { size: true } ); const group = groupQuery.data; const groupName = group?.Name ?? 'Environment group'; diff --git a/app/react/portainer/environments/environment-groups/ItemView/GroupHeader.tsx b/app/react/portainer/environments/environment-groups/ItemView/GroupHeader.tsx index 4be84b12f..df6a1229c 100644 --- a/app/react/portainer/environments/environment-groups/ItemView/GroupHeader.tsx +++ b/app/react/portainer/environments/environment-groups/ItemView/GroupHeader.tsx @@ -1,16 +1,18 @@ -import { Layers, Plus, RefreshCw, Trash2 } from 'lucide-react'; +import { LayoutGrid, Plus, RefreshCw, Trash2 } from 'lucide-react'; -import { useTags } from '@/portainer/tags/queries'; - -import { Button } from '@@/buttons'; import { ResourceDetailHeader } from '@@/ResourceDetailHeader/ResourceDetailHeader'; -import { Badge } from '@@/Badge'; +import { ResourceStatBlock } from '@@/ResourceDetailHeader/ResourceStatBlock'; +import { ActionBarShell } from '@@/ResourceDetailHeader/ActionBarShell'; +import { ActionBarButton } from '@@/ResourceDetailHeader/ActionBarButton'; +import { HeaderStats } from '@@/ResourceDetailHeader/HeaderStats'; import { EnvironmentGroup } from '../types'; +import { PlatformBadge } from '../components/PlatformBadge'; +import { EnvironmentTypeBreakdown } from '../components/EnvironmentTypeBreakdown'; interface Props { group?: EnvironmentGroup; - isLoading: boolean; + isLoading?: boolean; onRefresh?: () => void; onAddEnvironments?: () => void; onDelete?: () => void; @@ -23,50 +25,34 @@ export function GroupHeader({ onAddEnvironments, onDelete, }: Props) { - const tagsQuery = useTags(); - - const tagBadges = group?.TagIds?.length - ? group.TagIds.map((tagId) => { - const tag = tagsQuery.data?.find((t) => t.ID === tagId); - return ( - - {tag?.Name ?? `Tag ${tagId}`} - - ); - }) - : undefined; - const actionBar = group ? ( - <> +
- - +
- +
- +
) : undefined; return ( @@ -75,13 +61,25 @@ export function GroupHeader({ errorMessage={ !isLoading && !group ? 'Failed to load group details' : undefined } - icon={} - iconBackgroundClassName="bg-blue-3 th-dark:bg-blue-9" + icon={ + + } subtitleLabel="Environment Group" - subtitleClassName="text-blue-9 th-dark:text-blue-5" title={group?.Name || ''} - badge={tagBadges ? <>{tagBadges} : undefined} + badge={group && } description={group?.Description} + rightInfo={ + group && ( + + + Environments + + + + + + ) + } actionBar={actionBar} /> ); diff --git a/app/react/portainer/environments/environment-groups/ItemView/tabs/EnvironmentsTab.tsx b/app/react/portainer/environments/environment-groups/ItemView/tabs/EnvironmentsTab.tsx index d57e90436..804849601 100644 --- a/app/react/portainer/environments/environment-groups/ItemView/tabs/EnvironmentsTab.tsx +++ b/app/react/portainer/environments/environment-groups/ItemView/tabs/EnvironmentsTab.tsx @@ -44,7 +44,7 @@ export function EnvironmentsTab({ return ( <> -
+
{groupQuery.data && ( diff --git a/app/react/portainer/environments/environment-groups/components/PlatformBadge.tsx b/app/react/portainer/environments/environment-groups/components/PlatformBadge.tsx index dee14ef3a..6e25027db 100644 --- a/app/react/portainer/environments/environment-groups/components/PlatformBadge.tsx +++ b/app/react/portainer/environments/environment-groups/components/PlatformBadge.tsx @@ -2,11 +2,11 @@ import { EnvironmentGroup } from '../types'; import { getPlatformLabel } from '../utils/getPlatformLabel'; const colorByLabel: Record = { - Docker: 'bg-green-2', - Kubernetes: 'bg-blue-2', - Podman: 'bg-orange-2', - Mixed: 'bg-purple-2', - Empty: 'bg-gray-2', + Docker: 'bg-green-2 th-dark:bg-green-9', + Kubernetes: 'bg-blue-2 th-dark:bg-blue-9', + Podman: 'bg-orange-2 th-dark:bg-orange-9', + Mixed: 'bg-purple-2 th-dark:bg-purple-9', + Empty: 'bg-gray-2 th-dark:bg-gray-8', }; interface Props { @@ -15,11 +15,12 @@ interface Props { export function PlatformBadge({ group }: Props) { const platformLabel = getPlatformLabel(group); - const colorClass = colorByLabel[platformLabel] ?? 'bg-gray-2'; + const colorClass = + colorByLabel[platformLabel] ?? 'bg-gray-2 th-dark:bg-gray-8'; return ( {platformLabel} diff --git a/app/react/portainer/environments/environment-groups/queries/query-keys.ts b/app/react/portainer/environments/environment-groups/queries/query-keys.ts index e1cc6b98e..bcea887f4 100644 --- a/app/react/portainer/environments/environment-groups/queries/query-keys.ts +++ b/app/react/portainer/environments/environment-groups/queries/query-keys.ts @@ -3,5 +3,6 @@ import { EnvironmentGroupId } from '../../types'; export const queryKeys = { base: () => ['environment-groups'] as const, list: (size = false) => [...queryKeys.base(), { size }] as const, - group: (id?: EnvironmentGroupId) => [...queryKeys.base(), id] as const, + group: (id?: EnvironmentGroupId, size = false) => + [...queryKeys.base(), id, { size }] as const, }; diff --git a/app/react/portainer/environments/environment-groups/queries/useGroup.ts b/app/react/portainer/environments/environment-groups/queries/useGroup.ts index f15c8d3d6..e382be05a 100644 --- a/app/react/portainer/environments/environment-groups/queries/useGroup.ts +++ b/app/react/portainer/environments/environment-groups/queries/useGroup.ts @@ -13,18 +13,23 @@ export function useGroup( { select, enabled = true, + size = false, }: { select?: (group: PortainerEndpointGroup | null) => T; enabled?: boolean; + size?: boolean; } = {} ) { return useQuery( - queryKeys.group(groupId), + queryKeys.group(groupId, size), async () => { if (groupId === undefined) { return null; } - const { data } = await getEndpointGroupsById({ path: { id: groupId } }); + const { data } = await getEndpointGroupsById({ + path: { id: groupId }, + query: { size }, + }); return data; }, diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts index a23c53a72..b353d4aa7 100644 --- a/app/react/portainer/environments/environment.service/index.ts +++ b/app/react/portainer/environments/environment.service/index.ts @@ -1,4 +1,5 @@ import { endpointList } from '@api/sdk.gen'; +import { PortainerEndpoint } from '@api/types.gen'; import axios, { parseAxiosError } from '@/portainer/services/axios/axios'; import { @@ -170,10 +171,13 @@ export async function getAgentVersions() { export async function getEndpoint(id: EnvironmentId, excludeSnapshot = true) { try { - const { data: endpoint } = await axios.get(buildUrl(id), { - params: { excludeSnapshot }, - }); - return endpoint; + const { data: endpoint } = await axios.get( + buildUrl(id), + { + params: { excludeSnapshot }, + } + ); + return toEnvironment(endpoint); } catch (e) { throw parseAxiosError(e as Error); } diff --git a/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts b/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts index 4c9c86acd..d6a361010 100644 --- a/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts +++ b/app/react/portainer/environments/queries/useUpdateEnvironmentMutation.ts @@ -1,17 +1,19 @@ import { useQueryClient, useMutation } from '@tanstack/react-query'; -import { EndpointsEndpointUpdatePayload } from '@api/types.gen'; +import { + EndpointsEndpointUpdatePayload, + PortainerEndpoint, +} from '@api/types.gen'; import { withError, withInvalidate } from '@/react-tools/react-query'; import { EnvironmentId, - Environment, EnvironmentStatusMessage, KubernetesSettings, } from '@/react/portainer/environments/types'; import axios, { parseAxiosError } from '@/portainer/services/axios/axios'; -import { buildUrl } from '../environment.service/utils'; +import { buildUrl, toEnvironment } from '../environment.service/utils'; import { environmentQueryKeys } from './query-keys'; @@ -51,12 +53,9 @@ export async function updateEnvironment({ payload.TLSKey ); - const { data: endpoint } = await axios.put( - buildUrl(id), - payload - ); + const { data } = await axios.put(buildUrl(id), payload); - return endpoint; + return toEnvironment(data); } catch (e) { throw parseAxiosError(e as Error, 'Unable to update environment'); } diff --git a/app/react/portainer/generated-api/portainer/sdk.gen.ts b/app/react/portainer/generated-api/portainer/sdk.gen.ts index 158a830c1..bfb2d06c9 100644 --- a/app/react/portainer/generated-api/portainer/sdk.gen.ts +++ b/app/react/portainer/generated-api/portainer/sdk.gen.ts @@ -1008,6 +1008,7 @@ import { zGetApplicationsResourcesQuery, zGetApplicationsResourcesResponse, zGetEndpointGroupsByIdPath, + zGetEndpointGroupsByIdQuery, zGetEndpointGroupsByIdResponse, zGetKubernetesConfigMapPath, zGetKubernetesConfigMapResponse, @@ -1189,6 +1190,7 @@ import { zRestartKubernetesPodPath, zRestartKubernetesPodResponse, zRestoreBody, + zRestoreHeaders, zRoleListResponse, zSetDefaultKubernetesStorageClassPath, zSetDefaultKubernetesStorageClassResponse, @@ -1326,6 +1328,7 @@ import { zUploadTlsResponse, zUserAdminCheckResponse, zUserAdminInitBody, + zUserAdminInitHeaders, zUserAdminInitResponse, zUserCreateBody, zUserCreateResponse, @@ -2859,7 +2862,7 @@ export const getEndpointGroupsById = ( .object({ body: z.never().optional(), path: zGetEndpointGroupsByIdPath, - query: z.never().optional(), + query: zGetEndpointGroupsByIdQuery.optional(), }) .parseAsync(data), responseType: 'json', @@ -3875,7 +3878,7 @@ export const gitOpsSourcesList = ( /** * Delete a source * - * Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow. + * Deletes an existing GitOps source. Returns 409 if the source is referenced by any workflow or custom template. * **Access policy**: admin */ export const gitOpsSourcesDelete = ( @@ -7524,7 +7527,7 @@ export const resourceControlUpdate = ( * Triggers a system restore using provided backup file * * Triggers a system restore using provided backup file - * **Access policy**: public + * **Access policy**: public (requires the X-Setup-Token header on an uninitialized instance unless --no-setup-token is set) */ export const restore = ( options: Options @@ -7538,6 +7541,7 @@ export const restore = ( await z .object({ body: zRestoreBody, + headers: zRestoreHeaders.optional(), path: z.never().optional(), query: z.never().optional(), }) @@ -8096,9 +8100,9 @@ export const stackStart = ( }); /** - * Stops a stopped Stack + * Stop a running Stack * - * Stops a stopped Stack. + * Stop a running Stack. * **Access policy**: authenticated */ export const stackStop = ( @@ -9811,7 +9815,7 @@ export const userAdminCheck = ( * Initialize administrator account * * Initialize the 'admin' user account. - * **Access policy**: public + * **Access policy**: public (requires the X-Setup-Token header on an uninitialized instance unless --no-setup-token is set) */ export const userAdminInit = ( options: Options @@ -9825,6 +9829,7 @@ export const userAdminInit = ( await z .object({ body: zUserAdminInitBody, + headers: zUserAdminInitHeaders.optional(), path: z.never().optional(), query: z.never().optional(), }) diff --git a/app/react/portainer/generated-api/portainer/types.gen.ts b/app/react/portainer/generated-api/portainer/types.gen.ts index e913b3a61..b01b2fceb 100644 --- a/app/react/portainer/generated-api/portainer/types.gen.ts +++ b/app/react/portainer/generated-api/portainer/types.gen.ts @@ -263,18 +263,6 @@ export type V1ListMeta = { selfLink?: string; }; -export type V1Duration = { - 'time.Duration'?: - | -9223372036854776000 - | 9223372036854776000 - | 1 - | 1000 - | 1000000 - | 1000000000 - | 60000000000 - | 3600000000000; -}; - export type V1OwnerReference = { /** * API version of the referent. @@ -615,7 +603,7 @@ export type V1Beta1PodMetrics = { * collected from the interval [Timestamp-Window, Timestamp]. */ timestamp?: string; - window?: V1Duration; + window?: string; }; export type V1Beta1NodeMetricsList = { @@ -680,7 +668,7 @@ export type V1Beta1NodeMetrics = { * The memory usage is the memory working set. */ usage?: V1ResourceList; - window?: V1Duration; + window?: string; }; export type V1WindowsSecurityContextOptions = { @@ -4567,6 +4555,10 @@ export type SettingsPublicSettingsResponse = { * The minimum required length for a password of any user when using internal auth mode */ RequiredPasswordLength?: number; + /** + * Whether the setup wizard must send the X-Setup-Token header for admin init / restore + */ + RequiresSetupToken?: boolean; /** * Whether team sync is enabled */ @@ -5320,7 +5312,7 @@ export type PortainerUserThemeSettings = { /** * Color represents the color theme of the UI */ - color?: 'dark' | 'light' | 'highcontrast' | 'auto'; + color?: 'dark' | 'light' | 'highcontrast' | 'auto' | ''; }; export const PortainerUserRole = { @@ -5360,38 +5352,18 @@ export type PortainerResourceAccessLevel = (typeof PortainerResourceAccessLevel)[keyof typeof PortainerResourceAccessLevel]; export type PortainerUser = { - /** - * Deprecated in DBVersion == 25 - */ - EndpointAuthorizations?: PortainerEndpointAuthorizations; /** * User Identifier */ - Id?: number; - /** - * Deprecated in DBVersion == 25 - */ - PortainerAuthorizations?: PortainerAuthorizations; + Id: number; /** * User role (1 for administrator account and 2 for regular account) */ - Role?: PortainerUserRole; + Role: PortainerUserRole; ThemeSettings?: PortainerUserThemeSettings; TokenIssueAt?: number; UseCache?: boolean; - /** - * Deprecated - */ - UserTheme?: string; - Username?: string; -}; - -export type PortainerAuthorizations = { - [key: string]: boolean; -}; - -export type PortainerEndpointAuthorizations = { - [key: string]: PortainerAuthorizations; + Username: string; }; export type PortainerTeamResourceAccess = { @@ -6020,6 +5992,10 @@ export type PortainerRole = { Priority?: number; }; +export type PortainerAuthorizations = { + [key: string]: boolean; +}; + export type PortainerPerformanceMetrics = { CPUUsage?: number; DiskUsage?: number; @@ -6638,6 +6614,7 @@ export type PortainerCustomTemplatePlatform = (typeof PortainerCustomTemplatePlatform)[keyof typeof PortainerCustomTemplatePlatform]; export type PortainerCustomTemplate = { + ArtifactSources?: PortainerArtifactSources; /** * User identifier who created this template */ @@ -6695,6 +6672,19 @@ export type PortainerCustomTemplate = { Variables?: Array; }; +export type PortainerArtifact = { + configFilePath?: string; + configHash?: string; + edgeStackId?: number; + referenceName?: string; + stackId?: number; +}; + +export type PortainerArtifactSources = { + artifact?: PortainerArtifact; + sourceIds?: Array; +}; + export type MotdMotd = { ContentLayout?: { [key: string]: string; @@ -9733,7 +9723,12 @@ export type GetEndpointGroupsByIdData = { */ id: number; }; - query?: never; + query?: { + /** + * If true, include the number of environments and breakdown by type + */ + size?: boolean; + }; url: '/endpoint_groups/{id}'; }; @@ -9756,7 +9751,7 @@ export type GetEndpointGroupsByIdResponses = { /** * Success */ - 200: PortainerEndpointGroup; + 200: EndpointgroupsEndpointGroupResponse; }; export type GetEndpointGroupsByIdResponse = @@ -11201,7 +11196,7 @@ export type GitOpsSourcesDeleteErrors = { */ 404: unknown; /** - * Source is in use by one or more workflows + * Source is in use by one or more workflows or custom templates */ 409: unknown; /** @@ -15837,6 +15832,12 @@ export type RestoreData = { * Restore request payload */ body: BackupRestorePayload; + headers?: { + /** + * Setup token (required when instance is uninitialized and --no-setup-token is not set) + */ + 'X-Setup-Token'?: string; + }; path?: never; query?: never; url: '/restore'; @@ -15847,6 +15848,10 @@ export type RestoreErrors = { * Invalid request */ 400: unknown; + /** + * Access denied - invalid or missing setup token + */ + 403: unknown; /** * Server error */ @@ -18374,6 +18379,12 @@ export type UserAdminInitData = { * User details */ body: UsersAdminInitPayload; + headers?: { + /** + * Setup token (required when instance is uninitialized and --no-setup-token is not set) + */ + 'X-Setup-Token'?: string; + }; path?: never; query?: never; url: '/users/admin/init'; @@ -18384,6 +18395,10 @@ export type UserAdminInitErrors = { * Invalid request */ 400: unknown; + /** + * Access denied - invalid or missing setup token + */ + 403: unknown; /** * Admin user already initialized */ diff --git a/app/react/portainer/generated-api/portainer/zod.gen.ts b/app/react/portainer/generated-api/portainer/zod.gen.ts index b5435152a..e18d9625b 100644 --- a/app/react/portainer/generated-api/portainer/zod.gen.ts +++ b/app/react/portainer/generated-api/portainer/zod.gen.ts @@ -151,21 +151,6 @@ export const zV1ListMeta = z.object({ selfLink: z.string().optional(), }); -export const zV1Duration = z.object({ - 'time.Duration': z - .union([ - z.literal(-9223372036854776000), - z.literal(9223372036854776000), - z.literal(1), - z.literal(1000), - z.literal(1000000), - z.literal(1000000000), - z.literal(60000000000), - z.literal(3600000000000), - ]) - .optional(), -}); - export const zV1OwnerReference = z.object({ apiVersion: z.string().optional(), blockOwnerDeletion: z.boolean().optional(), @@ -226,7 +211,7 @@ export const zV1Beta1PodMetrics = z.object({ kind: z.string().optional(), metadata: zV1ObjectMeta.optional(), timestamp: z.string().optional(), - window: zV1Duration.optional(), + window: z.string().optional(), }); export const zV1Beta1PodMetricsList = z.object({ @@ -242,7 +227,7 @@ export const zV1Beta1NodeMetrics = z.object({ metadata: zV1ObjectMeta.optional(), timestamp: z.string().optional(), usage: zV1ResourceList.optional(), - window: zV1Duration.optional(), + window: z.string().optional(), }); export const zV1Beta1NodeMetricsList = z.object({ @@ -1327,6 +1312,7 @@ export const zSettingsPublicSettingsResponse = z.object({ OAuthLoginURI: z.string().optional(), OAuthLogoutURI: z.string().optional(), RequiredPasswordLength: z.int().optional(), + RequiresSetupToken: z.boolean().optional(), TeamSync: z.boolean().optional(), }); @@ -1637,7 +1623,7 @@ export const zPortainerWebhook = z.object({ }); export const zPortainerUserThemeSettings = z.object({ - color: z.enum(['dark', 'light', 'highcontrast', 'auto']).optional(), + color: z.enum(['dark', 'light', 'highcontrast', 'auto', '']).optional(), }); export const zPortainerUserRole = z.enum(PortainerUserRole); @@ -1651,23 +1637,13 @@ export const zPortainerUserResourceAccess = z.object({ UserId: z.int().optional(), }); -export const zPortainerAuthorizations = z.record(z.string(), z.boolean()); - -export const zPortainerEndpointAuthorizations = z.record( - z.string(), - zPortainerAuthorizations -); - export const zPortainerUser = z.object({ - EndpointAuthorizations: zPortainerEndpointAuthorizations.optional(), - Id: z.int().optional(), - PortainerAuthorizations: zPortainerAuthorizations.optional(), - Role: zPortainerUserRole.optional(), + Id: z.int(), + Role: zPortainerUserRole, ThemeSettings: zPortainerUserThemeSettings.optional(), TokenIssueAt: z.int().optional(), UseCache: z.boolean().optional(), - UserTheme: z.string().optional(), - Username: z.string().optional(), + Username: z.string(), }); export const zPortainerTeamResourceAccess = z.object({ @@ -1885,6 +1861,8 @@ export const zPortainerSslSettings = z.object({ selfSigned: z.boolean().optional(), }); +export const zPortainerAuthorizations = z.record(z.string(), z.boolean()); + export const zPortainerRole = z.object({ Authorizations: zPortainerAuthorizations.optional(), Description: z.string().optional(), @@ -2181,7 +2159,21 @@ export const zPortainerCustomTemplatePlatform = z.enum( PortainerCustomTemplatePlatform ); +export const zPortainerArtifact = z.object({ + configFilePath: z.string().optional(), + configHash: z.string().optional(), + edgeStackId: z.int().optional(), + referenceName: z.string().optional(), + stackId: z.int().optional(), +}); + +export const zPortainerArtifactSources = z.object({ + artifact: zPortainerArtifact.optional(), + sourceIds: z.array(z.int()).optional(), +}); + export const zPortainerCustomTemplate = z.object({ + ArtifactSources: zPortainerArtifactSources.optional(), CreatedByUserId: z.int().optional(), Description: z.string().optional(), EdgeTemplate: z.boolean().optional(), @@ -3574,10 +3566,15 @@ export const zGetEndpointGroupsByIdPath = z.object({ id: z.int(), }); +export const zGetEndpointGroupsByIdQuery = z.object({ + size: z.boolean().optional(), +}); + /** * Success */ -export const zGetEndpointGroupsByIdResponse = zPortainerEndpointGroup; +export const zGetEndpointGroupsByIdResponse = + zEndpointgroupsEndpointGroupResponse; /** * EndpointGroup details @@ -5286,6 +5283,10 @@ export const zResourceControlUpdateResponse = zPortainerResourceControl; */ export const zRestoreBody = zBackupRestorePayload; +export const zRestoreHeaders = z.object({ + 'X-Setup-Token': z.string().optional(), +}); + /** * Success */ @@ -5997,6 +5998,10 @@ export const zUserAdminCheckResponse = z.void(); */ export const zUserAdminInitBody = zUsersAdminInitPayload; +export const zUserAdminInitHeaders = z.object({ + 'X-Setup-Token': z.string().optional(), +}); + /** * Success */ diff --git a/app/react/sidebar/SidebarItem/SidebarItem.tsx b/app/react/sidebar/SidebarItem/SidebarItem.tsx index 7b875be9a..16e661c82 100644 --- a/app/react/sidebar/SidebarItem/SidebarItem.tsx +++ b/app/react/sidebar/SidebarItem/SidebarItem.tsx @@ -20,6 +20,9 @@ interface Props extends AutomationTestingProps { label: string; isSubMenu?: boolean; ignorePaths?: string[]; + /** When a create or detail path id differs from the list path, includePaths can be used to specify which paths should also mark the item as active. + * + * E.g. including the portainer.wizard.endpoints (create environment) path to activate the portainer.endpoints sidebar item. */ includePaths?: string[]; count?: number; } diff --git a/go.mod b/go.mod index 0f2f2bb9f..93ad5a24e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/portainer/portainer -go 1.26.2 +go 1.26.3 replace github.com/robfig/cron/v3 => github.com/robfig/cron/v3 v3.0.1 // Not actively maintained. Pinned to last known good version. Review needed when upgrading. @@ -16,7 +16,7 @@ require ( github.com/aws/smithy-go v1.24.2 github.com/cbroglie/mustache v1.4.0 github.com/compose-spec/compose-go/v2 v2.9.1 - github.com/containerd/containerd v1.7.30 + github.com/containerd/containerd v1.7.32 github.com/containerd/errdefs v1.0.0 github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 github.com/distribution/reference v0.6.0 @@ -48,6 +48,7 @@ require ( github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 github.com/prometheus/prometheus v0.311.3 + github.com/quasilyte/go-ruleguard/dsl v0.3.23 github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.34.0 github.com/samber/slog-zerolog/v2 v2.9.1 @@ -126,12 +127,12 @@ require ( github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/console v1.0.5 // indirect - github.com/containerd/containerd/api v1.9.0 // indirect - github.com/containerd/containerd/v2 v2.1.5 // indirect + github.com/containerd/containerd/api v1.10.0 // indirect + github.com/containerd/containerd/v2 v2.2.4 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/containerd/platforms v1.0.0-rc.2 // indirect github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect @@ -150,7 +151,7 @@ require ( github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect @@ -272,7 +273,7 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/novln/docker-parser v1.0.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect - github.com/opencontainers/runtime-spec v1.2.1 // indirect + github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/openshift/api v3.9.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect @@ -314,7 +315,7 @@ require ( github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect @@ -341,7 +342,7 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - golang.org/x/net v0.54.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect @@ -371,5 +372,5 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect - tags.cncf.io/container-device-interface v1.0.1 // indirect + tags.cncf.io/container-device-interface v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 4addcb24a..17ab4f159 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 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/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= -github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= +github.com/Microsoft/hcsshim v0.14.1/go.mod h1:VnzvPLyWUhxiPVsJ31P6XadxCcTogTguBFDy/1GR/OM= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= @@ -187,16 +187,16 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AX github.com/compose-spec/compose-go/v2 v2.9.1 h1:8UwI+ujNU+9Ffkf/YgAm/qM9/eU7Jn8nHzWG721W4rs= github.com/compose-spec/compose-go/v2 v2.9.1/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= -github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= +github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= -github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= -github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= -github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= -github.com/containerd/containerd/v2 v2.1.5 h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro= -github.com/containerd/containerd/v2 v2.1.5/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8= +github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E= +github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= +github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= +github.com/containerd/containerd/v2 v2.2.4 h1:8x2UdXqww7NYqGNabQ7i1nAgB5LegzjC9KQzO/900iA= +github.com/containerd/containerd/v2 v2.2.4/go.mod h1:YBcTO8D9149QY9zNmUjy04Mhuc4DlrZQ8FIOwKZEM7o= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -209,8 +209,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= -github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= -github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4= @@ -294,8 +294,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= @@ -771,8 +771,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= -github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/openshift/api v3.9.0+incompatible h1:fJ/KsefYuZAjmrr3+5U9yZIZbTOpVkDDLDLFresAeYs= @@ -841,6 +841,8 @@ github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuX github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU= github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo= github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= +github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= +github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= @@ -976,8 +978,9 @@ 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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 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= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= @@ -1119,8 +1122,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1283,5 +1286,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WO sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/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= -tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= -tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= +tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY= +tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q= diff --git a/package.json b/package.json index 809eee06a..ca9a77237 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,15 @@ "packageManager": "pnpm@10.26.2", "dependencies": { "@aws-crypto/sha256-js": "^2.0.0", - "@codemirror/autocomplete": "^6.4.0", - "@codemirror/commands": "^6.8.0", - "@codemirror/language": "^6.3.2", - "@codemirror/legacy-modes": "^6.3.1", - "@codemirror/lint": "^6.8.4", - "@codemirror/search": "^6.2.3", - "@codemirror/state": "^6.2.0", - "@codemirror/theme-one-dark": "^6.1.0", - "@codemirror/view": "^6.7.1", + "@codemirror/autocomplete": "^6.20.2", + "@codemirror/commands": "^6.10.3", + "@codemirror/language": "^6.12.3", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/lint": "^6.9.6", + "@codemirror/search": "^6.7.0", + "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.43.0", "@lezer/common": "^1.0.2", "@lezer/highlight": "^1.1.3", "@nxmix/tokenize-ansi": "^3.0.0", @@ -62,8 +62,8 @@ "@uirouter/angularjs": "1.0.11", "@uirouter/react": "^1.0.7", "@uirouter/react-hybrid": "^1.0.4", - "@uiw/codemirror-themes": "^4.19.9", - "@uiw/react-codemirror": "^4.19.5", + "@uiw/codemirror-themes": "^4.25.10", + "@uiw/react-codemirror": "^4.25.10", "@wojtekmaj/react-daterange-picker": "^5.5.0", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", @@ -79,7 +79,7 @@ "angular-ui-bootstrap": "~2.5.0", "angularjs-scroll-glue": "^2.2.0", "angularjs-slider": "^6.4.0", - "axios": "^1.13.5", + "axios": "^1.16.1", "axios-cache-interceptor": "^1.11.2", "axios-progress-bar": "git://github.com/portainer/progress-bar-4-axios", "babel-plugin-angularjs-annotate": "^0.10.0", @@ -91,7 +91,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^1.1.1", "codemirror": "^6.0.1", - "codemirror-json-schema": "^0.8.0", + "codemirror-json-schema": "^0.8.1", "core-js": "^3.19.3", "date-fns": "^2.29.3", "docker-types": "~1.47", @@ -99,7 +99,7 @@ "file-saver": "^2.0.5", "filesize": "^10.1.6", "filesize-parser": "^1.5.0", - "formik": "^2.2.9", + "formik": "^2.4.9", "globals": "^15.15.0", "i18next": "^21.3.3", "i18next-browser-languagedetector": "^6.1.2", @@ -108,12 +108,12 @@ "js-base64": "^3.7.2", "jsdom": "^24", "json-schema": "^0.4.0", - "lodash": "^4.17.21", - "lodash-es": "npm:lodash@4.17.21", + "lodash": "^4.18.1", + "lodash-es": "npm:lodash@4.18.1", "lucide-react": "^0.577.0", "markdown-to-jsx": "^7.7.4", - "moment": "^2.29.1", - "moment-timezone": "^0.5.40", + "moment": "^2.30.1", + "moment-timezone": "^0.6.2", "mustache": "^4.2.0", "ng-file-upload": "~12.2.13", "parse-duration": "^2.1.4", @@ -126,8 +126,8 @@ "react-dom": "^17.0.2", "react-i18next": "^11.12.0", "react-is": "^17.0.2", - "react-json-view-lite": "^1.2.1", - "react-select": "^5.2.1", + "react-json-view-lite": "^1.5.0", + "react-select": "^5.10.2", "react-select-async-paginate": "^0.7.11", "sanitize-html": "^2.17.0", "spinkit": "^2.0.1", @@ -155,26 +155,26 @@ "@eslint/js": "^9.39.2", "@hey-api/openapi-ts": "0.97.3", "@simbathesailor/use-what-changed": "^2.0.0", - "@storybook/addon-docs": "10.3.5", - "@storybook/addon-links": "10.3.5", + "@storybook/addon-docs": "10.4.2", + "@storybook/addon-links": "10.4.2", "@storybook/addon-styling-webpack": "^3.0.2", "@storybook/addon-webpack5-compiler-swc": "^4.0.3", - "@storybook/react-webpack5": "10.3.5", + "@storybook/react-webpack5": "10.4.2", "@svgr/webpack": "^8.1.0", "@testing-library/dom": "^9.3.4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^12", "@testing-library/react-hooks": "^8.0.1", - "@testing-library/user-event": "^14.5.2", + "@testing-library/user-event": "^14.6.1", "@types/angular": "^1.8.3", "@types/file-saver": "^2.0.4", "@types/filesize": "^5.0.2", "@types/filesize-parser": "^1.5.1", "@types/jquery": "^3.5.10", "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.21", + "@types/lodash": "^4.17.24", "@types/mustache": "^4.1.2", - "@types/node": "^22.19.11", + "@types/node": "^25.9.1", "@types/nprogress": "^0.2.0", "@types/qs": "^6.9.17", "@types/react": "^17.0.37", @@ -184,19 +184,19 @@ "@types/sanitize-html": "^2.8.0", "@types/toastr": "^2.1.39", "@types/uuid": "^3.3.2", - "@typescript-eslint/eslint-plugin": "^8.58.2", - "@typescript-eslint/parser": "^8.58.2", - "@vitest/coverage-v8": "^4", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", + "@vitest/coverage-v8": "^4.1.8", "@vitest/eslint-plugin": "^1.6.15", "auto-ngtemplate-loader": "^3.1.2", - "autoprefixer": "^10.4.16", + "autoprefixer": "^10.5.0", "babel-loader": "^9.1.3", "babel-plugin-i18next-extract": "^0.9.0", "babel-plugin-lodash": "^3.3.4", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", - "cssnano": "^6.0.1", + "cssnano": "^8.0.1", "dotenv-webpack": "^8.0.1", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", @@ -205,11 +205,11 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-promise": "^7.3.0", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-regex": "^1.10.0", - "eslint-plugin-storybook": "10.3.5", + "eslint-plugin-storybook": "10.4.2", "html-loader": "^5.1.0", "html-webpack-plugin": "^5.5.3", "husky": "^8.0.0", @@ -217,45 +217,43 @@ "lint-staged": "^14.0.1", "lodash-webpack-plugin": "^0.11.6", "mini-css-extract-plugin": "^2.7.6", - "msw": "^2.12.10", + "msw": "^2.14.6", "msw-storybook-addon": "^2.0.7", "ngtemplate-loader": "^2.1.0", - "postcss": "^8.5.6", + "postcss": "^8.5.15", "postcss-loader": "^7.3.3", "prettier": "^3.8.3", "prettier-plugin-tailwindcss": "^0.8.0", "react-docgen-typescript-plugin": "^1.0.5", "source-map-loader": "^4.0.1", "speed-measure-webpack-plugin": "^1.5.0", - "storybook": "10.3.5", + "storybook": "10.4.2", "storybook-css-modules-preset": "^1.1.1", "style-loader": "^3.3.3", "swagger2openapi": "^7.0.8", "tailwindcss": "3.3.3", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.2", - "vite": "^8.0.8", + "typescript-eslint": "^8.60.1", + "vite": "^8.0.16", "vite-plugin-svgr": "^4.5.0", "vite-tsconfig-paths": "^4.3.1", - "vitest": "^4", - "webpack": "^5.105.0", + "vitest": "^4.1.8", + "webpack": "^5.107.2", "webpack-build-notifier": "^3.1.0", "webpack-bundle-analyzer": "^5.2.0", "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.3", + "webpack-dev-server": "^5.2.4", "webpack-merge": "^6.0.1" }, "pnpm": { "overrides": { "jquery": "^3.6.0", "decompress": "^4.2.1", - "lodash": "^4.17.21", "minimist": "^1.2.6", "http-proxy": "^1.18.1", "daterangepicker>jquery": "^3.5.1", "@uirouter/react": "^1.0.7", "@uirouter/angularjs": "1.0.11", - "moment": "^2.21.0", "markdown-it": "^12.3.2", "@types/react": "^17.0.37", "@types/react-dom": "^17.0.11", diff --git a/pkg/libhttp/ssrf/ssrf.go b/pkg/libhttp/ssrf/ssrf.go new file mode 100644 index 000000000..15a3ca1e1 --- /dev/null +++ b/pkg/libhttp/ssrf/ssrf.go @@ -0,0 +1,252 @@ +package ssrf + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "sync/atomic" + + "github.com/rs/zerolog/log" +) + +// Mode controls how the SSRF policy is applied. +type Mode string + +const ( + // ModeOff disables SSRF protection entirely. All connections pass through unchanged. + ModeOff Mode = "off" + // ModeAudit resolves and checks destinations but only logs violations; connections are allowed. + ModeAudit Mode = "audit" + // ModeEnforce blocks connections that violate the policy. + ModeEnforce Mode = "enforce" +) + +// Policy defines the SSRF protection policy for outbound HTTP connections. +type Policy struct { + // Mode controls whether protection is off, in audit-only mode, or enforcing. + Mode Mode + + // AllowedHosts is the allowlist of permitted destinations. + // Accepted formats: + // - Exact hostname: "example.com" + // - Wildcard hostname: "*.example.com" (matches any subdomain at any depth) + // - Single IP: "1.2.3.4" + // - CIDR range: "10.0.0.0/8" + // + // When Mode is ModeEnforce and AllowedHosts is empty, all outbound connections are blocked. + AllowedHosts []string +} + +type safeDialer struct { + base net.Dialer + mode Mode + allowedNets []*net.IPNet + allowedHosts map[string]bool + allowedWilds []string // derived from "*.foo.com" entries; stored as ".foo.com" +} + +var globalDialer atomic.Pointer[safeDialer] + +// Configure initializes the global SSRF policy. Intended to be called once +// at startup before any outbound HTTP connections are established. +func Configure(policy Policy) { + if policy.Mode == ModeOff || policy.Mode == "" { + globalDialer.Store(nil) + return + } + + globalDialer.Store(newSafeDialer(policy)) +} + +// IsEnabled reports whether SSRF protection is currently active (audit or enforce). +func IsEnabled() bool { + return globalDialer.Load() != nil +} + +// CheckURL validates rawURL against the active SSRF policy without making a +// connection. Returns nil when protection is disabled or the destination is +// permitted. In audit mode, logs a warning on violations and returns nil. +func CheckURL(ctx context.Context, rawURL string) error { + d := globalDialer.Load() + if d == nil { + return nil + } + + u, err := url.Parse(rawURL) + if err != nil { + return fmt.Errorf("ssrf: invalid URL %q: %w", rawURL, err) + } + + host := u.Hostname() + if host == "" { + return nil + } + + return d.checkHost(ctx, host) +} + +// WrapTransport clones t and replaces its DialContext with the global SSRF-filtering +// dialer. Returns t unchanged when SSRF protection is not configured. +func WrapTransport(t *http.Transport) *http.Transport { + d := globalDialer.Load() + if d == nil { + return t + } + + cloned := t.Clone() + cloned.DialContext = d.DialContext + + return cloned +} + +// WrapTransportInternal is a documented no-op for transports that connect to +// internally computed destinations (local Docker socket proxy, Chisel tunnels, +// in-cluster Kubernetes API). The destination is chosen by Portainer, not +// supplied by any user, so SSRF validation is not applicable. Using this +// function instead of WrapTransport makes the exemption explicit and +// satisfies the ruleguard lint rule. +func WrapTransportInternal(t *http.Transport) *http.Transport { + return t +} + +func newSafeDialer(policy Policy) *safeDialer { + allowedNets := make([]*net.IPNet, 0, len(policy.AllowedHosts)) + allowedHosts := make(map[string]bool, len(policy.AllowedHosts)) + var allowedWilds []string + + for _, entry := range policy.AllowedHosts { + if _, network, err := net.ParseCIDR(entry); err == nil { + allowedNets = append(allowedNets, network) + continue + } + + if ip := net.ParseIP(entry); ip != nil { + bits := 32 + if ip.To4() == nil { + bits = 128 + } + + mask := net.CIDRMask(bits, bits) + allowedNets = append(allowedNets, &net.IPNet{IP: ip.Mask(mask), Mask: mask}) + + continue + } + + if strings.HasPrefix(entry, "*.") { + allowedWilds = append(allowedWilds, entry[1:]) // "*.foo.com" -> ".foo.com" + continue + } + + allowedHosts[entry] = true + } + + return &safeDialer{ + mode: policy.Mode, + allowedNets: allowedNets, + allowedHosts: allowedHosts, + allowedWilds: allowedWilds, + } +} + +// DialContext resolves addr, validates all resolved IPs against the allowlist policy, +// then dials using the first resolved IP to prevent DNS rebinding attacks. +func (d *safeDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("ssrf: invalid address %q: %w", addr, err) + } + + resolved, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, fmt.Errorf("ssrf: resolving %q: %w", host, err) + } + + if len(resolved) == 0 { + return nil, fmt.Errorf("ssrf: no addresses resolved for %q", host) + } + + // Dial by resolved IP regardless of how the host was allowed to close the + // window between DNS validation and the TCP handshake (DNS rebinding). + dialTarget := net.JoinHostPort(resolved[0].IP.String(), port) + + if d.allowedHosts[host] || d.matchesWildcard(host) { + return d.base.DialContext(ctx, network, dialTarget) + } + + for _, a := range resolved { + if !d.ipAllowed(a.IP) { + if d.mode == ModeAudit { + log.Warn().Str("host", host).Str("ip", a.IP.String()).Msg("ssrf: destination not in allowlist (audit mode, allowing)") + continue + } + + return nil, fmt.Errorf("ssrf: destination %s is not in the allowlist", a.IP) + } + } + + return d.base.DialContext(ctx, network, dialTarget) +} + +func (d *safeDialer) checkHost(ctx context.Context, host string) error { + if d.allowedHosts[host] || d.matchesWildcard(host) { + return nil + } + + if ip := net.ParseIP(host); ip != nil { + if !d.ipAllowed(ip) { + if d.mode == ModeAudit { + log.Warn().Str("host", host).Msg("ssrf: destination not in allowlist (audit mode, allowing)") + return nil + } + + return fmt.Errorf("ssrf: destination %s is not in the allowlist", ip) + } + + return nil + } + + resolved, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return fmt.Errorf("ssrf: resolving %q: %w", host, err) + } + + if len(resolved) == 0 { + return fmt.Errorf("ssrf: no addresses resolved for %q", host) + } + + for _, a := range resolved { + if !d.ipAllowed(a.IP) { + if d.mode == ModeAudit { + log.Warn().Str("host", host).Str("ip", a.IP.String()).Msg("ssrf: destination not in allowlist (audit mode, allowing)") + continue + } + + return fmt.Errorf("ssrf: destination %s is not in the allowlist", a.IP) + } + } + + return nil +} + +func (d *safeDialer) matchesWildcard(host string) bool { + for _, suffix := range d.allowedWilds { + if strings.HasSuffix(host, suffix) { + return true + } + } + + return false +} + +func (d *safeDialer) ipAllowed(ip net.IP) bool { + for _, network := range d.allowedNets { + if network.Contains(ip) { + return true + } + } + + return false +} diff --git a/pkg/libhttp/ssrf/ssrf_test.go b/pkg/libhttp/ssrf/ssrf_test.go new file mode 100644 index 000000000..329d63225 --- /dev/null +++ b/pkg/libhttp/ssrf/ssrf_test.go @@ -0,0 +1,357 @@ +package ssrf + +import ( + "context" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIpAllowed_CIDR(t *testing.T) { + t.Parallel() + + d := newSafeDialer(Policy{ + Mode: ModeEnforce, + AllowedHosts: []string{"8.8.0.0/16", "2001:4860::/32"}, + }) + + require.True(t, d.ipAllowed(net.ParseIP("8.8.8.8"))) + require.True(t, d.ipAllowed(net.ParseIP("8.8.4.4"))) + require.True(t, d.ipAllowed(net.ParseIP("2001:4860:4860::8888"))) + + require.False(t, d.ipAllowed(net.ParseIP("1.1.1.1"))) + require.False(t, d.ipAllowed(net.ParseIP("127.0.0.1"))) + require.False(t, d.ipAllowed(net.ParseIP("169.254.169.254"))) +} + +func TestIpAllowed_SingleIP(t *testing.T) { + t.Parallel() + + d := newSafeDialer(Policy{ + Mode: ModeEnforce, + AllowedHosts: []string{"1.2.3.4"}, + }) + + require.True(t, d.ipAllowed(net.ParseIP("1.2.3.4"))) + require.False(t, d.ipAllowed(net.ParseIP("1.2.3.5"))) +} + +func TestMatchesWildcard(t *testing.T) { + t.Parallel() + + d := newSafeDialer(Policy{ + Mode: ModeEnforce, + AllowedHosts: []string{"*.example.com", "exact.host.com"}, + }) + + require.True(t, d.matchesWildcard("foo.example.com")) + require.True(t, d.matchesWildcard("bar.example.com")) + require.True(t, d.matchesWildcard("deep.nested.example.com")) + + require.False(t, d.matchesWildcard("example.com")) + require.False(t, d.matchesWildcard("notexample.com")) + require.False(t, d.matchesWildcard("exact.host.com")) +} + +func TestNewSafeDialer_MixedHosts(t *testing.T) { + t.Parallel() + + d := newSafeDialer(Policy{ + Mode: ModeEnforce, + AllowedHosts: []string{"example.com", "*.internal.net", "10.0.0.0/8", "1.2.3.4"}, + }) + + require.True(t, d.allowedHosts["example.com"]) + require.Contains(t, d.allowedWilds, ".internal.net") + require.Len(t, d.allowedNets, 2) // 10.0.0.0/8 and 1.2.3.4/32 +} + +func TestConfigure_Disabled(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + require.NotNil(t, globalDialer.Load()) + + Configure(Policy{}) + require.Nil(t, globalDialer.Load()) +} + +func TestWrapTransport_NoPolicy(t *testing.T) { + globalDialer.Store(nil) + + base := &http.Transport{} + result := WrapTransport(base) + require.Equal(t, base, result) +} + +func TestWrapTransport_WithPolicy(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + base := &http.Transport{} + result := WrapTransport(base) + require.NotEqual(t, base, result) + require.NotNil(t, result.DialContext) +} + +func TestCheckURL_Disabled(t *testing.T) { + globalDialer.Store(nil) + + err := CheckURL(t.Context(), "http://169.254.169.254/latest/meta-data/") + require.NoError(t, err) +} + +func TestCheckURL_BlocksIPNotInAllowlist(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"8.8.8.0/24"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://169.254.169.254/latest/meta-data/") + require.Error(t, err) + require.Contains(t, err.Error(), "ssrf") +} + +func TestCheckURL_AllowedHostname(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "https://example.com/path") + require.NoError(t, err) +} + +func TestCheckURL_AuditMode_ReturnsNil(t *testing.T) { + Configure(Policy{Mode: ModeAudit, AllowedHosts: []string{"8.8.8.0/24"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://169.254.169.254/latest/meta-data/") + require.NoError(t, err) +} + +// TestDialContext_BlocksLoopback is an end-to-end test: it starts a real HTTP +// server on 127.0.0.1, enables SSRF protection with an allowlist that does not +// include loopback, and verifies that the wrapped transport blocks the request. +func TestDialContext_BlocksLoopback(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"8.8.8.8"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + blocked := &http.Client{Transport: WrapTransport(&http.Transport{})} + resp, err := blocked.Get(srv.URL) + require.Error(t, err) + require.Contains(t, err.Error(), "ssrf") + if resp != nil { + require.NoError(t, resp.Body.Close()) + } + + Configure(Policy{}) + + open := &http.Client{Transport: WrapTransport(&http.Transport{})} + resp, err = open.Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) +} + +// TestDialContext_AuditMode_AllowsLoopback verifies that audit mode logs the +// violation but still allows the connection through. +func TestDialContext_AuditMode_AllowsLoopback(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + Configure(Policy{Mode: ModeAudit, AllowedHosts: []string{"8.8.8.8"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + client := &http.Client{Transport: WrapTransport(&http.Transport{})} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) +} + +func TestIsEnabled(t *testing.T) { + globalDialer.Store(nil) + require.False(t, IsEnabled()) + + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + require.True(t, IsEnabled()) +} + +func TestWrapTransportInternal(t *testing.T) { + t.Parallel() + + base := &http.Transport{} + result := WrapTransportInternal(base) + require.Equal(t, base, result) +} + +func TestNewSafeDialer_IPv6SingleIP(t *testing.T) { + t.Parallel() + + d := newSafeDialer(Policy{ + Mode: ModeEnforce, + AllowedHosts: []string{"::1"}, + }) + + require.True(t, d.ipAllowed(net.ParseIP("::1"))) + require.False(t, d.ipAllowed(net.ParseIP("::2"))) +} + +func TestCheckURL_InvalidURL(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://%gg") + require.Error(t, err) + require.Contains(t, err.Error(), "ssrf") +} + +func TestCheckURL_EmptyHost(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://") + require.NoError(t, err) +} + +// TestCheckURL_IPInAllowlist verifies that a literal IP address that falls +// within an allowed CIDR range is permitted. +func TestCheckURL_IPInAllowlist(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"8.8.8.0/24"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://8.8.8.8/path") + require.NoError(t, err) +} + +func TestCheckURL_WildcardHostname(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"*.example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "https://api.example.com/path") + require.NoError(t, err) +} + +// TestCheckURL_HostnameDNSResolvesToAllowedIP verifies that a hostname +// resolving to an IP within the allowlist is permitted (DNS resolution path). +func TestCheckURL_HostnameDNSResolvesToAllowedIP(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"127.0.0.0/8", "::1/128"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://localhost/path") + require.NoError(t, err) +} + +// TestCheckURL_HostnameDNSResolvesToBlockedIP verifies that a hostname +// resolving to an IP outside the allowlist is blocked (DNS resolution path). +func TestCheckURL_HostnameDNSResolvesToBlockedIP(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"8.8.8.0/24"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://localhost/path") + require.Error(t, err) + require.Contains(t, err.Error(), "ssrf") +} + +// TestCheckURL_HostnameDNSAuditMode verifies that audit mode logs violations +// from hostname DNS resolution but still returns nil. +func TestCheckURL_HostnameDNSAuditMode(t *testing.T) { + Configure(Policy{Mode: ModeAudit, AllowedHosts: []string{"8.8.8.0/24"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + err := CheckURL(t.Context(), "http://localhost/path") + require.NoError(t, err) +} + +// TestCheckURL_HostnameDNSError verifies that a DNS resolution failure is +// propagated as an SSRF-prefixed error. +func TestCheckURL_HostnameDNSError(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"8.8.8.0/24"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + err := CheckURL(ctx, "http://portainer-nonexistent.test.invalid/path") + require.Error(t, err) +} + +// TestDialContext_InvalidAddress verifies that an address without a port +// returns an SSRF-prefixed error. +func TestDialContext_InvalidAddress(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"example.com"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + d := globalDialer.Load() + _, err := d.DialContext(t.Context(), "tcp", "no-port-here") + require.Error(t, err) + require.Contains(t, err.Error(), "ssrf") +} + +// TestDialContext_DNSError verifies that a DNS resolution failure in +// DialContext is propagated as an SSRF-prefixed error. +func TestDialContext_DNSError(t *testing.T) { + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + d := globalDialer.Load() + _, err := d.DialContext(ctx, "tcp", "portainer-nonexistent.test.invalid:80") + require.Error(t, err) +} + +// TestDialContext_AllowedByCIDR is an end-to-end test verifying that +// connections to IPs within an allowed CIDR range are permitted. +func TestDialContext_AllowedByCIDR(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"127.0.0.0/8"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + client := &http.Client{Transport: WrapTransport(&http.Transport{})} + resp, err := client.Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) +} + +// TestDialContext_AllowedByExactHostname verifies that when a hostname is in +// the allowed-hosts list, the connection is permitted even though the resolved +// IP is not covered by any CIDR entry. +// +// The server is bound to whatever IP "localhost" resolves to first so that the +// dialTarget computed by DialContext (resolved[0]) matches the listening address. +func TestDialContext_AllowedByExactHostname(t *testing.T) { + addrs, err := net.DefaultResolver.LookupIPAddr(t.Context(), "localhost") + require.NoError(t, err) + require.NotEmpty(t, addrs, "localhost must resolve to at least one address") + + l, err := net.Listen("tcp", net.JoinHostPort(addrs[0].IP.String(), "0")) + require.NoError(t, err) + + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + srv.Listener = l + srv.Start() + defer srv.Close() + + _, portStr, err := net.SplitHostPort(l.Addr().String()) + require.NoError(t, err) + + Configure(Policy{Mode: ModeEnforce, AllowedHosts: []string{"localhost"}}) + t.Cleanup(func() { globalDialer.Store(nil) }) + + client := &http.Client{Transport: WrapTransport(&http.Transport{})} + resp, err := client.Get("http://localhost:" + portStr) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) +} diff --git a/pkg/libpolicy/fingerprint.go b/pkg/libpolicy/fingerprint.go new file mode 100644 index 000000000..3dba6f2db --- /dev/null +++ b/pkg/libpolicy/fingerprint.go @@ -0,0 +1,43 @@ +package libpolicy + +import ( + "hash/fnv" + "sort" + "strconv" + + "github.com/segmentio/encoding/json" + + portainer "github.com/portainer/portainer/api" +) + +// ConfigFingerprint is the generic primitive: FNV-1a over serialized policy +// config bytes. It is a change detector, not a security boundary; collisions +// are acceptable because a later poll or policy edit will retry reconciliation. +// Callers must ensure the bytes are deterministic (e.g. collections sorted +// before marshaling). +// +// New policy types should call this after marshaling their own config struct. +func ConfigFingerprint(config []byte) string { + h := fnv.New32a() + h.Write(config) + return strconv.FormatUint(uint64(h.Sum32()), 16) +} + +// HelmPolicyFingerprint computes a fingerprint for a Helm policy from its chart +// summaries. Charts are sorted by name before hashing so the result is +// order-independent. The restore manifest is excluded because it does not +// affect what gets installed. +// +// Both server-ee and the agent import this function to guarantee they compute +// identical fingerprints for the same chart summaries. +func HelmPolicyFingerprint(chartSummaries []portainer.PolicyChartSummary) string { + sorted := make([]portainer.PolicyChartSummary, len(chartSummaries)) + copy(sorted, chartSummaries) + sort.Slice(sorted, func(i, j int) bool { return sorted[i].ChartName < sorted[j].ChartName }) + b, err := json.Marshal(portainer.HelmPolicyConfig{Charts: sorted}) + if err != nil { + // json.Marshal of a known struct with no custom marshalers cannot error. + return "" + } + return ConfigFingerprint(b) +} diff --git a/pkg/registryhttp/client.go b/pkg/registryhttp/client.go index 519d2fd30..cedab57b1 100644 --- a/pkg/registryhttp/client.go +++ b/pkg/registryhttp/client.go @@ -4,6 +4,7 @@ import ( "net/http" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/pkg/libhttp/ssrf" "github.com/rs/zerolog/log" "oras.land/oras-go/v2/registry/remote/retry" @@ -15,8 +16,8 @@ import ( func CreateClient(registry *portainer.Registry) (httpClient *http.Client, usePlainHttp bool, err error) { switch registry.Type { case portainer.AzureRegistry, portainer.EcrRegistry, portainer.GithubRegistry, portainer.GitlabRegistry, portainer.DockerHubRegistry: - // Cloud registries use the default retry client with built-in TLS - return retry.DefaultClient, false, nil + base := http.DefaultTransport.(*http.Transport).Clone() + return &http.Client{Transport: retry.NewTransport(ssrf.WrapTransport(base))}, false, nil default: // For all other registry types, use shared helper to build transport and scheme diff --git a/pkg/registryhttp/client_test.go b/pkg/registryhttp/client_test.go index b6d494c9f..abe780658 100644 --- a/pkg/registryhttp/client_test.go +++ b/pkg/registryhttp/client_test.go @@ -11,6 +11,11 @@ import ( "oras.land/oras-go/v2/registry/remote/retry" ) +func isRetryTransport(t *http.Client) bool { + _, ok := t.Transport.(*retry.Transport) + return ok +} + func init() { fips.InitFIPS(false) } @@ -102,8 +107,7 @@ func TestCreateClient(t *testing.T) { // Verify client type based on registry configuration switch tt.registry.Type { case portainer.AzureRegistry, portainer.EcrRegistry, portainer.GithubRegistry, portainer.GitlabRegistry: - // Cloud registries should use the default retry client - assert.Equal(t, retry.DefaultClient, client) + assert.True(t, isRetryTransport(client), "Cloud registries should use a retry transport") } }) } @@ -133,7 +137,7 @@ func TestCreateClient_CloudRegistries(t *testing.T) { require.NoError(t, err) assert.NotNil(t, client) assert.False(t, usePlainHTTP, "Cloud registries should use HTTPS") - assert.Equal(t, retry.DefaultClient, client, "Cloud registries should use default retry client") + assert.True(t, isRetryTransport(client), "Cloud registries should use a retry transport") }) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb2f7026f..19dd19bd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,13 +7,11 @@ settings: overrides: jquery: ^3.6.0 decompress: ^4.2.1 - lodash: ^4.17.21 minimist: ^1.2.6 http-proxy: ^1.18.1 daterangepicker>jquery: ^3.5.1 '@uirouter/react': ^1.0.7 '@uirouter/angularjs': 1.0.11 - moment: ^2.21.0 markdown-it: ^12.3.2 '@types/react': ^17.0.37 '@types/react-dom': ^17.0.11 @@ -29,32 +27,32 @@ importers: specifier: ^2.0.0 version: 2.0.1 '@codemirror/autocomplete': - specifier: ^6.4.0 - version: 6.18.6 + specifier: ^6.20.2 + version: 6.20.2 '@codemirror/commands': - specifier: ^6.8.0 - version: 6.8.0 + specifier: ^6.10.3 + version: 6.10.3 '@codemirror/language': - specifier: ^6.3.2 - version: 6.10.8 + specifier: ^6.12.3 + version: 6.12.3 '@codemirror/legacy-modes': - specifier: ^6.3.1 - version: 6.4.2 + specifier: ^6.5.3 + version: 6.5.3 '@codemirror/lint': - specifier: ^6.8.4 - version: 6.8.4 + specifier: ^6.9.6 + version: 6.9.6 '@codemirror/search': - specifier: ^6.2.3 - version: 6.5.8 + specifier: ^6.7.0 + version: 6.7.0 '@codemirror/state': - specifier: ^6.2.0 - version: 6.5.1 + specifier: ^6.6.0 + version: 6.6.0 '@codemirror/theme-one-dark': - specifier: ^6.1.0 - version: 6.1.2 + specifier: ^6.1.3 + version: 6.1.3 '@codemirror/view': - specifier: ^6.7.1 - version: 6.36.2 + specifier: ^6.43.0 + version: 6.43.0 '@lezer/common': specifier: ^1.0.2 version: 1.2.3 @@ -104,11 +102,11 @@ importers: specifier: ^1.0.4 version: 1.0.4(angular@1.8.2)(react@17.0.2) '@uiw/codemirror-themes': - specifier: ^4.19.9 - version: 4.23.7(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) + specifier: ^4.25.10 + version: 4.25.10(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) '@uiw/react-codemirror': - specifier: ^4.19.5 - version: 4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.2)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + specifier: ^4.25.10 + version: 4.25.10(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@wojtekmaj/react-daterange-picker': specifier: ^5.5.0 version: 5.5.0(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -155,14 +153,14 @@ importers: specifier: ^6.4.0 version: 6.7.0(angular@1.8.2) axios: - specifier: ^1.13.5 - version: 1.13.5 + specifier: ^1.16.1 + version: 1.16.1 axios-cache-interceptor: specifier: ^1.11.2 - version: 1.11.2(axios@1.13.5) + version: 1.11.2(axios@1.16.1) axios-progress-bar: specifier: git://github.com/portainer/progress-bar-4-axios - version: https://codeload.github.com/portainer/progress-bar-4-axios/tar.gz/d424cd29f5b629af9ffc5c3d6db8d92d11d82f0f(axios@1.13.5) + version: https://codeload.github.com/portainer/progress-bar-4-axios/tar.gz/d424cd29f5b629af9ffc5c3d6db8d92d11d82f0f(axios@1.16.1) babel-plugin-angularjs-annotate: specifier: ^0.10.0 version: 0.10.0 @@ -191,8 +189,8 @@ importers: specifier: ^6.0.1 version: 6.0.1 codemirror-json-schema: - specifier: ^0.8.0 - version: 0.8.0(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)(@lezer/common@1.2.3) + specifier: ^0.8.1 + version: 0.8.1(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.2.3) core-js: specifier: ^3.19.3 version: 3.20.0 @@ -215,8 +213,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 formik: - specifier: ^2.2.9 - version: 2.2.9(react@17.0.2) + specifier: ^2.4.9 + version: 2.4.9(@types/react@17.0.75)(react@17.0.2) globals: specifier: ^15.15.0 version: 15.15.0 @@ -242,11 +240,11 @@ importers: specifier: ^0.4.0 version: 0.4.0 lodash: - specifier: ^4.17.21 - version: 4.17.21 + specifier: ^4.18.1 + version: 4.18.1 lodash-es: - specifier: npm:lodash@4.17.21 - version: lodash@4.17.21 + specifier: npm:lodash@4.18.1 + version: lodash@4.18.1 lucide-react: specifier: ^0.577.0 version: 0.577.0(react@17.0.2) @@ -254,11 +252,11 @@ importers: specifier: ^7.7.4 version: 7.7.4(react@17.0.2) moment: - specifier: ^2.21.0 - version: 2.29.4 + specifier: ^2.30.1 + version: 2.30.1 moment-timezone: - specifier: ^0.5.40 - version: 0.5.41 + specifier: ^0.6.2 + version: 0.6.2 mustache: specifier: ^4.2.0 version: 4.2.0 @@ -282,7 +280,7 @@ importers: version: 4.8.0(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react-codemirror-merge: specifier: ^4.23.11 - version: 4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.2)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + version: 4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react-datetime-picker: specifier: ^5.6.0 version: 5.6.0(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -296,14 +294,14 @@ importers: specifier: ^17.0.2 version: 17.0.2 react-json-view-lite: - specifier: ^1.2.1 - version: 1.2.1(react@17.0.2) + specifier: ^1.5.0 + version: 1.5.0(react@17.0.2) react-select: - specifier: ^5.2.1 - version: 5.2.1(@babel/core@7.23.6)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + specifier: ^5.10.2 + version: 5.10.2(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) react-select-async-paginate: specifier: ^0.7.11 - version: 0.7.11(@types/react@17.0.75)(react-select@5.2.1(@babel/core@7.23.6)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) + version: 0.7.11(@types/react@17.0.75)(react-select@5.10.2(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) sanitize-html: specifier: ^2.17.0 version: 2.17.0 @@ -358,13 +356,13 @@ importers: version: 7.23.0(@babel/core@7.23.6) '@chromatic-com/storybook': specifier: ^5.1.2 - version: 5.1.2(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + version: 5.1.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) '@eslint-community/eslint-plugin-eslint-comments': specifier: ^4.7.1 - version: 4.7.1(eslint@9.39.4(jiti@2.6.1)) + version: 4.7.1(eslint@9.39.4(jiti@2.7.0)) '@eslint/compat': specifier: ^2.0.0 - version: 2.0.5(eslint@9.39.4(jiti@2.6.1)) + version: 2.0.5(eslint@9.39.4(jiti@2.7.0)) '@eslint/eslintrc': specifier: ^3.3.3 version: 3.3.5 @@ -378,20 +376,20 @@ importers: specifier: ^2.0.0 version: 2.0.0(react@17.0.2) '@storybook/addon-docs': - specifier: 10.3.5 - version: 10.3.5(@types/react@17.0.75)(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.105.0) + specifier: 10.4.2 + version: 10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.107.2) '@storybook/addon-links': - specifier: 10.3.5 - version: 10.3.5(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + specifier: 10.4.2 + version: 10.4.2(@types/react@17.0.75)(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) '@storybook/addon-styling-webpack': specifier: ^3.0.2 - version: 3.0.2(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.105.0) + version: 3.0.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.107.2) '@storybook/addon-webpack5-compiler-swc': specifier: ^4.0.3 - version: 4.0.3(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.105.0) + version: 4.0.3(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.107.2) '@storybook/react-webpack5': - specifier: 10.3.5 - version: 10.3.5(@swc/core@1.15.11)(esbuild@0.27.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1) + specifier: 10.4.2 + version: 10.4.2(@swc/core@1.15.11)(@types/react-dom@17.0.25)(@types/react@17.0.75)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1) '@svgr/webpack': specifier: ^8.1.0 version: 8.1.0 @@ -408,8 +406,8 @@ importers: specifier: ^8.0.1 version: 8.0.1(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@testing-library/user-event': - specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@9.3.4) + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@9.3.4) '@types/angular': specifier: ^1.8.3 version: 1.8.9 @@ -429,8 +427,8 @@ importers: specifier: ^7.0.15 version: 7.0.15 '@types/lodash': - specifier: ^4.17.21 - version: 4.17.21 + specifier: ^4.17.24 + version: 4.17.24 '@types/mustache': specifier: ^4.1.2 version: 4.2.6 @@ -465,26 +463,26 @@ importers: specifier: ^3.3.2 version: 3.4.13 '@typescript-eslint/eslint-plugin': - specifier: ^8.58.2 - version: 8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + specifier: ^8.60.1 + version: 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) '@typescript-eslint/parser': - specifier: ^8.58.2 - version: 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + specifier: ^8.60.1 + version: 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) '@vitest/coverage-v8': - specifier: ^4 + specifier: ^4.1.8 version: 4.1.8(vitest@4.1.8) '@vitest/eslint-plugin': specifier: ^1.6.15 - version: 1.6.19(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)(vitest@4.1.8) + version: 1.6.15(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)(vitest@4.1.8) auto-ngtemplate-loader: specifier: ^3.1.2 - version: 3.1.2(webpack@5.105.0) + version: 3.1.2(webpack@5.107.2) autoprefixer: - specifier: ^10.4.16 - version: 10.4.16(postcss@8.5.6) + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.15) babel-loader: specifier: ^9.1.3 - version: 9.1.3(@babel/core@7.23.6)(webpack@5.105.0) + version: 9.1.3(@babel/core@7.23.6)(webpack@5.107.2) babel-plugin-i18next-extract: specifier: ^0.9.0 version: 0.9.0 @@ -493,61 +491,61 @@ importers: version: 3.3.4 clean-webpack-plugin: specifier: ^4.0.0 - version: 4.0.0(webpack@5.105.0) + version: 4.0.0(webpack@5.107.2) copy-webpack-plugin: specifier: ^11.0.0 - version: 11.0.0(webpack@5.105.0) + version: 11.0.0(webpack@5.107.2) css-loader: specifier: ^6.8.1 - version: 6.8.1(webpack@5.105.0) + version: 6.8.1(webpack@5.107.2) cssnano: - specifier: ^6.0.1 - version: 6.0.1(postcss@8.5.6) + specifier: ^8.0.1 + version: 8.0.1(postcss@8.5.15) dotenv-webpack: specifier: ^8.0.1 - version: 8.0.1(webpack@5.105.0) + version: 8.0.1(webpack@5.107.2) eslint: specifier: ^9.39.4 - version: 9.39.4(jiti@2.6.1) + version: 9.39.4(jiti@2.7.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.4(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-alias: specifier: ^1.1.2 version: 1.1.2(eslint-plugin-import@2.32.0) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-eslint-comments: specifier: ^3.2.0 - version: 3.2.0(eslint@9.39.4(jiti@2.6.1)) + version: 3.2.0(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + version: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-promise: - specifier: ^7.2.1 - version: 7.2.1(eslint@9.39.4(jiti@2.6.1)) + specifier: ^7.3.0 + version: 7.3.0(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@2.6.1)) + version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.4(jiti@2.6.1)) + specifier: ^7.1.1 + version: 7.1.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-regex: specifier: ^1.10.0 - version: 1.10.0(eslint@9.39.4(jiti@2.6.1)) + version: 1.10.0(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-storybook: - specifier: 10.3.5 - version: 10.3.5(eslint@9.39.4(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2) + specifier: 10.4.2 + version: 10.4.2(eslint@9.39.4(jiti@2.7.0))(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2) html-loader: specifier: ^5.1.0 - version: 5.1.0(webpack@5.105.0) + version: 5.1.0(webpack@5.107.2) html-webpack-plugin: specifier: ^5.5.3 - version: 5.5.3(webpack@5.105.0) + version: 5.5.3(webpack@5.107.2) husky: specifier: ^8.0.0 version: 8.0.3 @@ -559,25 +557,25 @@ importers: version: 14.0.1 lodash-webpack-plugin: specifier: ^0.11.6 - version: 0.11.6(webpack@5.105.0) + version: 0.11.6(webpack@5.107.2) mini-css-extract-plugin: specifier: ^2.7.6 - version: 2.7.6(webpack@5.105.0) + version: 2.7.6(webpack@5.107.2) msw: - specifier: ^2.12.10 - version: 2.12.10(@types/node@25.0.3)(typescript@6.0.2) + specifier: ^2.14.6 + version: 2.14.6(@types/node@25.0.3)(typescript@6.0.2) msw-storybook-addon: specifier: ^2.0.7 - version: 2.0.7(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2)) + version: 2.0.7(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2)) ngtemplate-loader: specifier: ^2.1.0 version: 2.1.0 postcss: - specifier: ^8.5.6 - version: 8.5.6 + specifier: ^8.5.15 + version: 8.5.15 postcss-loader: specifier: ^7.3.3 - version: 7.3.3(postcss@8.5.6)(webpack@5.105.0) + version: 7.3.3(postcss@8.5.15)(webpack@5.107.2) prettier: specifier: ^3.8.3 version: 3.8.3 @@ -586,22 +584,22 @@ importers: version: 0.8.0(prettier@3.8.3) react-docgen-typescript-plugin: specifier: ^1.0.5 - version: 1.0.5(typescript@6.0.2)(webpack@5.105.0) + version: 1.0.5(typescript@6.0.2)(webpack@5.107.2) source-map-loader: specifier: ^4.0.1 - version: 4.0.1(webpack@5.105.0) + version: 4.0.1(webpack@5.107.2) speed-measure-webpack-plugin: specifier: ^1.5.0 - version: 1.5.0(webpack@5.105.0) + version: 1.5.0(webpack@5.107.2) storybook: - specifier: 10.3.5 - version: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + specifier: 10.4.2 + version: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) storybook-css-modules-preset: specifier: ^1.1.1 version: 1.1.1 style-loader: specifier: ^3.3.3 - version: 3.3.3(webpack@5.105.0) + version: 3.3.3(webpack@5.107.2) swagger2openapi: specifier: ^7.0.8 version: 7.0.8 @@ -612,35 +610,35 @@ importers: specifier: ^6.0.2 version: 6.0.2 typescript-eslint: - specifier: ^8.58.2 - version: 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + specifier: ^8.60.1 + version: 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) vite: - specifier: ^8.0.8 - version: 8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) + specifier: ^8.0.16 + version: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.54.0)(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) + version: 4.5.0(rollup@4.54.0)(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) vite-tsconfig-paths: specifier: ^4.3.1 - version: 4.3.1(typescript@6.0.2)(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) + version: 4.3.1(typescript@6.0.2)(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) vitest: - specifier: ^4 - version: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) + specifier: ^4.1.8 + version: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) webpack: - specifier: ^5.105.0 - version: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + specifier: ^5.107.2 + version: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) webpack-build-notifier: specifier: ^3.1.0 - version: 3.1.0(webpack@5.105.0) + version: 3.1.0(webpack@5.107.2) webpack-bundle-analyzer: specifier: ^5.2.0 version: 5.2.0 webpack-cli: specifier: ^6.0.1 - version: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0) + version: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2) webpack-dev-server: - specifier: ^5.2.3 - version: 5.2.3(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.105.0) + specifier: ^5.2.4 + version: 5.2.4(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.107.2) webpack-merge: specifier: ^6.0.1 version: 6.0.1 @@ -1414,11 +1412,11 @@ packages: peerDependencies: storybook: ^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 - '@codemirror/autocomplete@6.18.6': - resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==} + '@codemirror/autocomplete@6.20.2': + resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} - '@codemirror/commands@6.8.0': - resolution: {integrity: sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==} + '@codemirror/commands@6.10.3': + resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} '@codemirror/lang-json@6.0.1': resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} @@ -1426,29 +1424,32 @@ packages: '@codemirror/lang-yaml@6.1.2': resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} - '@codemirror/language@6.10.8': - resolution: {integrity: sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==} + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} - '@codemirror/legacy-modes@6.4.2': - resolution: {integrity: sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==} + '@codemirror/legacy-modes@6.5.3': + resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==} - '@codemirror/lint@6.8.4': - resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==} + '@codemirror/lint@6.9.6': + resolution: {integrity: sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==} '@codemirror/merge@6.10.0': resolution: {integrity: sha512-Omn0gU6MM5cKQGqgKoIhFjUqCNWH/nukCMLXzu/1jOdtiHsxAu3GdENBf1QYkoIC3FSgkF7X/ClOqYeUATQ4Sw==} - '@codemirror/search@6.5.8': - resolution: {integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==} + '@codemirror/search@6.7.0': + resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} - '@codemirror/state@6.5.1': - resolution: {integrity: sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==} + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} - '@codemirror/theme-one-dark@6.1.2': - resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==} + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} - '@codemirror/view@6.36.2': - resolution: {integrity: sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==} + '@codemirror/view@6.43.0': + resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} + + '@colordx/core@5.4.3': + resolution: {integrity: sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==} '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} @@ -1458,51 +1459,77 @@ packages: resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} engines: {node: '>=14.17.0'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + '@emotion/cache@11.6.0': resolution: {integrity: sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==} - '@emotion/hash@0.8.0': - resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} '@emotion/memoize@0.7.5': resolution: {integrity: sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==} - '@emotion/react@11.6.0': - resolution: {integrity: sha512-23MnRZFBN9+D1lHXC5pD6z4X9yhPxxtHr6f+iTGz6Fv6Rda0GdefPrsHL7otsEf+//7uqCdT5QtHeRxHCERzuw==} + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} peerDependencies: - '@babel/core': ^7.0.0 '@types/react': '*' react: '>=16.8.0' peerDependenciesMeta: - '@babel/core': - optional: true '@types/react': optional: true - '@emotion/serialize@1.0.2': - resolution: {integrity: sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==} + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} '@emotion/sheet@1.1.0': resolution: {integrity: sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==} - '@emotion/unitless@0.7.5': - resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' '@emotion/utils@1.0.0': resolution: {integrity: sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==} + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + '@emotion/weak-memoize@0.2.5': resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==} + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -1719,6 +1746,15 @@ packages: '@exodus/schemasafe@1.0.0-rc.6': resolution: {integrity: sha512-dDnQizD94EdBwEj/fh3zPRa/HWCS9O5au2PuHhZBbuM3xWHxuaKzPBOEWze7Nn0xW68MIpZ7Xdyn1CoCpjKCuQ==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@hey-api/codegen-core@0.8.2': resolution: {integrity: sha512-R2NMf3wq97rh1mjz33WJQU8svz3F0RYUjvx/QzXucjpSqQ3O5huTdDjErG4fMxSr1X+X56NuDrqtGfHmo1TRUQ==} engines: {node: '>=22.13.0'} @@ -1760,35 +1796,35 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/ansi@1.0.2': - resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': ^25 peerDependenciesMeta: '@types/node': optional: true - '@inquirer/core@10.3.2': - resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} - engines: {node: '>=18'} + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': ^25 peerDependenciesMeta: '@types/node': optional: true - '@inquirer/figures@1.0.15': - resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} - engines: {node: '>=18'} + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/type@3.0.10': - resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} - engines: {node: '>=18'} + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': ^25 peerDependenciesMeta: @@ -1962,6 +1998,9 @@ packages: '@lezer/common@1.2.3': resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + '@lezer/highlight@1.2.1': resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} @@ -1987,15 +2026,15 @@ packages: '@types/react': ^17.0.37 react: '>=16' - '@mswjs/interceptors@0.41.2': - resolution: {integrity: sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} engines: {node: '>=18'} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -2026,14 +2065,234 @@ packages: '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + '@open-draft/logger@0.3.0': resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@oxc-resolver/binding-android-arm-eabi@11.20.0': + resolution: {integrity: sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.20.0': + resolution: {integrity: sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.20.0': + resolution: {integrity: sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.20.0': + resolution: {integrity: sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.20.0': + resolution: {integrity: sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0': + resolution: {integrity: sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.20.0': + resolution: {integrity: sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.20.0': + resolution: {integrity: sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.20.0': + resolution: {integrity: sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': + resolution: {integrity: sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': + resolution: {integrity: sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': + resolution: {integrity: sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': + resolution: {integrity: sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.20.0': + resolution: {integrity: sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.20.0': + resolution: {integrity: sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.20.0': + resolution: {integrity: sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.20.0': + resolution: {integrity: sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': + resolution: {integrity: sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.20.0': + resolution: {integrity: sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw==} + cpu: [x64] + os: [win32] '@peculiar/asn1-cms@2.6.0': resolution: {integrity: sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==} @@ -2372,97 +2631,97 @@ packages: react: ^16.8.0 || 17.x react-dom: ^16.8.0 || 17.x - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} @@ -2627,17 +2886,24 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.3.5': - resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==} + '@storybook/addon-docs@10.4.2': + resolution: {integrity: sha512-CtW1O4xSKZPNtpWgpfp4yB/x4pj/of+3MvlEDfErSlr3Hp3QmEa2pCLaecR08H5LJqJFlt1PtG0UrIynTvgW9w==} peerDependencies: - storybook: ^10.3.5 - - '@storybook/addon-links@10.3.5': - resolution: {integrity: sha512-Xe2wCGZ+hpZ0cDqAIBHk+kPc8nODNbu585ghd5bLrlYJMDVXoNM/fIlkrLgjIDVbfpgeJLUEg7vldJrn+FyOLw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + '@types/react': ^17.0.37 + storybook: ^10.4.2 peerDependenciesMeta: + '@types/react': + optional: true + + '@storybook/addon-links@10.4.2': + resolution: {integrity: sha512-cU8h4/m+oAr8UUwF4teZG2N1ilV+vU+98Ii/Ma+IIx9M/V7i5544UxfAz84dV5Rx2Oho6x8XH3gIvmevSyPi/Q==} + peerDependencies: + '@types/react': ^17.0.37 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.2 + peerDependenciesMeta: + '@types/react': + optional: true react: optional: true @@ -2653,26 +2919,26 @@ packages: peerDependencies: storybook: ^9.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 - '@storybook/builder-webpack5@10.3.5': - resolution: {integrity: sha512-DYjIpfuwkl8CrDbYWjMcwxrLY3QpcZtDJr4ZcT3hrbZHF5BJ3HnVIv1YM+KF/bJfIUMS2h/YMsRyKVYGthiSzQ==} + '@storybook/builder-webpack5@10.4.2': + resolution: {integrity: sha512-nhmV0+nThCgy1y5742SS7c4vJrd5/1KfCXCNfsJ1v4Rkq7NIQnUhEIBwkSaY63lqH7FRHlFxIjwGS63veiCJuw==} peerDependencies: - storybook: ^10.3.5 + storybook: ^10.4.2 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@storybook/core-webpack@10.3.5': - resolution: {integrity: sha512-CEtGU2f6+FefIR3v4P1KBJB17UngZDSmib2w36jfVp1pNPIzqdIG2s1NCKAM7vbQHxXVcLpBH31mJqyU+vdypQ==} + '@storybook/core-webpack@10.4.2': + resolution: {integrity: sha512-qnYKMruU8lvI4yaq2PA9Gmxjrc7EZ3DRBI/cVKwEgOIREoxzr1F1IE7t7+325k9Phylue7E5rD3A7yjxeEKUyw==} peerDependencies: - storybook: ^10.3.5 + storybook: ^10.4.2 - '@storybook/csf-plugin@10.3.5': - resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==} + '@storybook/csf-plugin@10.4.2': + resolution: {integrity: sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.3.5 + storybook: ^10.4.2 vite: '*' webpack: '*' peerDependenciesMeta: @@ -2688,18 +2954,18 @@ packages: '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - '@storybook/icons@2.0.1': - resolution: {integrity: sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg==} + '@storybook/icons@2.0.2': + resolution: {integrity: sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/preset-react-webpack@10.3.5': - resolution: {integrity: sha512-PAlh2nJOY+yxYBUBAurWOdd+1RGhl8e5MhpC8hTNhaTB8//WKTpUAOyM8Q1PvflCYKU9Hz11nP4Q4jY1WVEoUA==} + '@storybook/preset-react-webpack@10.4.2': + resolution: {integrity: sha512-21ld380f0/jTTitkfhTKgP3FBnVAgMu1P1ymrRyiFYJVSJBA5YejndFFBo0ugq9iGGsHXrVdOphC/OJKbTSWRQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.4.2 typescript: '*' peerDependenciesMeta: typescript: @@ -2711,32 +2977,45 @@ packages: typescript: '>= 4.x' webpack: '>= 4' - '@storybook/react-dom-shim@10.3.5': - resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==} + '@storybook/react-dom-shim@10.4.2': + resolution: {integrity: sha512-Eng3Yt2NCjPX94QcfyLeUFhrMj0hec2yU9J/qafBVbfj9XrFI8o+0ZwYJ7uXb9ECbvPN4y06dgt/2W/LiR417w==} peerDependencies: + '@types/react': ^17.0.37 + '@types/react-dom': ^17.0.11 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.4.2 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@storybook/react-webpack5@10.3.5': - resolution: {integrity: sha512-g5nxwFBVjm59IDG8qu0mnIno7DJHKvBuJvwO/HyHSuaKvmNRkDCJ8mDegPhpaEwmHPX4dg1GDDUjZ4fwFbQWbA==} + '@storybook/react-webpack5@10.4.2': + resolution: {integrity: sha512-x7xwGLxU0w6/qi29/cHhua8qiCvfE05ku4pPLTXF8TsP/zfGsY8tbdlKO2+YKp+iBG8vafVc//ZXOAty1oypDA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.4.2 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@storybook/react@10.3.5': - resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==} + '@storybook/react@10.4.2': + resolution: {integrity: sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==} peerDependencies: + '@types/react': ^17.0.37 + '@types/react-dom': ^17.0.11 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.3.5 + storybook: ^10.4.2 typescript: '>= 4.9.x' peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true typescript: optional: true @@ -2965,12 +3244,6 @@ packages: react: <18.0.0 react-dom: <18.0.0 - '@testing-library/user-event@14.5.2': - resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - '@testing-library/user-event@14.6.1': resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} @@ -3008,9 +3281,6 @@ packages: '@types/babel__template@7.4.1': resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} - '@types/babel__traverse@7.20.4': - resolution: {integrity: sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==} - '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} @@ -3035,18 +3305,9 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@4.17.34': resolution: {integrity: sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==} @@ -3069,6 +3330,11 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': ^17.0.37 + '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} @@ -3099,8 +3365,8 @@ packages: '@types/lodash.memoize@4.1.9': resolution: {integrity: sha512-glY1nQuoqX4Ft8Uk+KfJudOD7DQbbEDF6k9XpGncaohW3RW4eSWBlx6AA0fZCrh40tZcQNH4jS/Oc59J6Eq+aw==} - '@types/lodash@4.17.21': - resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -3123,6 +3389,9 @@ packages: '@types/nprogress@0.2.3': resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/prop-types@15.7.4': resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==} @@ -3174,6 +3443,9 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/sizzle@2.3.10': resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} @@ -3201,63 +3473,100 @@ packages: '@types/yargs@16.0.4': resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==} - '@typescript-eslint/eslint-plugin@8.58.2': - resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.58.2 + '@typescript-eslint/parser': ^8.60.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.58.2': - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.58.2': - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + '@typescript-eslint/project-service@8.58.1': + resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.58.2': - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.58.2': - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.58.2': - resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} + '@typescript-eslint/scope-manager@8.58.1': + resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.1': + resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.58.2': - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + '@typescript-eslint/types@8.58.1': + resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.58.2': - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.1': + resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.58.2': - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.1': + resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.58.2': - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.1': + resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@uirouter/angularjs@1.0.11': @@ -3296,8 +3605,19 @@ packages: '@codemirror/state': '>=6.0.0' '@codemirror/view': '>=6.0.0' - '@uiw/codemirror-themes@4.23.7': - resolution: {integrity: sha512-UNf1XOx1hG9OmJnrtT86PxKcdcwhaNhbrcD+nsk8WxRJ3n5c8nH6euDvgVPdVLPwbizsaQcZTILACgA/FjRpVg==} + '@uiw/codemirror-extensions-basic-setup@4.25.10': + resolution: {integrity: sha512-P3vytLlpE62KYSWrMUnwDCv2lvaQDuDZzyj03mHntuHo5bSl34fRZpjTY3kQTPGuXHxkGSYpoPFFj+hMTqaaMQ==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/codemirror-themes@4.25.10': + resolution: {integrity: sha512-Fqiz1HIuDlDftcL+/O53V333UOH6MqQ84VbiQB5egn6u+uDwAqACp1FrdAoi4wgpR3b3TGW4Gr0wIYcrJSSz1A==} peerDependencies: '@codemirror/language': '>=6.0.0' '@codemirror/state': '>=6.0.0' @@ -3314,8 +3634,20 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@uiw/react-codemirror@4.25.10': + resolution: {integrity: sha512-DzgSMwM5qzB7v1FIb4gEeriYt67iiay756/HIOM9mAbeOVK0MO7rqefHf0O5c0269pJKMW7AH9FjclExD23V9w==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -3421,8 +3753,8 @@ packages: '@vitest/browser': optional: true - '@vitest/eslint-plugin@1.6.19': - resolution: {integrity: sha512-zodmXRsVKFsuHxHJILuTFaaKsrsxm0YsiOX65clk+LpCW9JrVXaf6ERXr0caDs+NEk0S62Jyk0K7XYQ7gWXheA==} + '@vitest/eslint-plugin@1.6.15': + resolution: {integrity: sha512-dTMjrdngmcB+DxomlKQ+SUubCTvd0m2hQQFpv5sx+GRodmeoxr2PVbphk57SVp250vpxphk9Ccwyv6fQ6+2gkA==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -3609,6 +3941,15 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -3830,8 +4171,8 @@ packages: resolution: {integrity: sha512-rAoaXKKj7bgZZI4RhpSTimgBv92hea1J9sygmwTFdy7fu8cP1/+xx6vCmW24+z52bmqVluKAJcVIPQ7ancXilA==} engines: {node: '>=12'} - autoprefixer@10.4.16: - resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -3861,8 +4202,8 @@ packages: peerDependencies: axios: ^1.6.2 - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -3885,6 +4226,10 @@ packages: babel-plugin-lodash@3.3.4: resolution: {integrity: sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==} + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + babel-plugin-polyfill-corejs2@0.4.7: resolution: {integrity: sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==} peerDependencies: @@ -3910,8 +4255,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.11: - resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} hasBin: true batch@0.6.1: @@ -3961,8 +4307,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -4043,8 +4389,8 @@ packages: caniuse-lite@1.0.30001751: resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} - caniuse-lite@1.0.30001762: - resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} @@ -4177,8 +4523,8 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - codemirror-json-schema@0.8.0: - resolution: {integrity: sha512-Hiww3eU/8zwPw6oWT2VNTT7w7vwWzFiE7+syleD8rYg5pCfKuGa2oiAAgZCXpx6EUJtIcq3LKP9gkEbNUD5jcA==} + codemirror-json-schema@0.8.1: + resolution: {integrity: sha512-4lKPjW+nugNAmM5MsggJyn6TUxYdCCwAJIr9T4cZeTFPdkbBvPteCOGtDedrTOIeTC2ZFJtVg7VHIXnYU32t8w==} peerDependencies: '@codemirror/language': ^6.10.2 '@codemirror/lint': ^6.8.0 @@ -4209,9 +4555,6 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true - colord@2.9.2: - resolution: {integrity: sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -4230,6 +4573,10 @@ packages: resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} engines: {node: '>=16'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -4285,6 +4632,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4322,6 +4672,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cosmiconfig@8.2.0: resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} engines: {node: '>=14'} @@ -4336,12 +4690,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-declaration-sorter@6.4.0: - resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} - engines: {node: ^10 || ^12 || >=14} - peerDependencies: - postcss: ^8.0.9 - css-loader@6.8.1: resolution: {integrity: sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==} engines: {node: '>= 12.13.0'} @@ -4374,6 +4722,10 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -4386,23 +4738,23 @@ packages: engines: {node: '>=4'} hasBin: true - cssnano-preset-default@6.0.1: - resolution: {integrity: sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ==} - engines: {node: ^14 || ^16 || >=18.0} + cssnano-preset-default@8.0.1: + resolution: {integrity: sha512-OTdKeYMlvQ8KBgyej5ysktnWJoeyo7rGrVnm+bdpIHGvxhbTGPsOkB+7T1EdTuX00dGlQQb2UEbSPB1OpMXULw==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - cssnano-utils@4.0.0: - resolution: {integrity: sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==} - engines: {node: ^14 || ^16 || >=18.0} + cssnano-utils@6.0.0: + resolution: {integrity: sha512-ztS9W/+uaDn+bkYmDhs+GdMveHJ3CL8IPNHpRqDUQXv5GJOTQAJjV1XUOInr9esLXSabQV1pLRZlJpyUwEqDyQ==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - cssnano@6.0.1: - resolution: {integrity: sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg==} - engines: {node: ^14 || ^16 || >=18.0} + cssnano@8.0.1: + resolution: {integrity: sha512-oSiOnPQNNYjusTUlYJiE6xvFQG4don3N0QavaoV1BxIsC1zjvxOwikXlR7lG1EVmZNDDaJkHbQx1VRB8kaoMHA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -4682,8 +5034,8 @@ packages: electron-to-chromium@1.4.616: resolution: {integrity: sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -4705,8 +5057,8 @@ packages: endent@2.1.0: resolution: {integrity: sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==} - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} entities@2.1.0: @@ -4794,10 +5146,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4893,17 +5241,17 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-promise@7.2.1: - resolution: {integrity: sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==} + eslint-plugin-promise@7.3.0: + resolution: {integrity: sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-react-hooks@7.0.1: - resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} engines: {node: '>=18'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -4917,11 +5265,11 @@ packages: peerDependencies: eslint: '>=4.0.0' - eslint-plugin-storybook@10.3.5: - resolution: {integrity: sha512-rEFkfU3ypF44GpB4tiJ9EFDItueoGvGi3+weLHZax2ON2MB7VIDsxdSUGvIU5tMURg+oWYlpzCyLm4TpDq2deA==} + eslint-plugin-storybook@10.4.2: + resolution: {integrity: sha512-l3/vzLRmb8VSi3X1Bo6/Pa+64naw1jFsZE5jPPA4izvVdNhH1rF4rGuOC3kDTU926qKVBQtKua8D24XWQtvcGg==} peerDependencies: eslint: '>=8' - storybook: ^10.3.5 + storybook: ^10.4.2 eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -5045,9 +5393,18 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastest-levenshtein@1.0.12: resolution: {integrity: sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==} @@ -5100,6 +5457,9 @@ packages: resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} engines: {node: '>=14.16'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5134,8 +5494,8 @@ packages: resolution: {integrity: sha512-pZ2bO++NWLHhiKkgP1bEXHhR1/OjVcSvlCJ98aNJDFeb7H5OOQaO+SKOZle6041O9rv2tmbrO4JzClAvDUHf0g==} engines: {node: '>=10'} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -5169,8 +5529,8 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - formik@2.2.9: - resolution: {integrity: sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==} + formik@2.4.9: + resolution: {integrity: sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==} peerDependencies: react: '>=16.8.0' @@ -5178,8 +5538,8 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.6: - resolution: {integrity: sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} @@ -5322,8 +5682,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + graphql@16.14.1: + resolution: {integrity: sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} growly@1.3.0: @@ -5383,8 +5743,8 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - headers-polyfill@4.0.2: - resolution: {integrity: sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -5455,10 +5815,6 @@ packages: resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} engines: {node: '>= 0.6'} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -5489,6 +5845,10 @@ packages: http2-client@1.3.5: resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -5886,8 +6246,8 @@ packages: resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} hasBin: true - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jquery@3.6.0: @@ -6089,6 +6449,10 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -6109,8 +6473,8 @@ packages: enquirer: optional: true - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} engines: {node: '>=6.11.5'} loader-utils@1.4.2: @@ -6161,8 +6525,11 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-update@5.0.1: resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} @@ -6242,6 +6609,9 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@1.0.1: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} @@ -6259,8 +6629,8 @@ packages: peerDependencies: tslib: '2' - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -6354,11 +6724,11 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - moment-timezone@0.5.41: - resolution: {integrity: sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg==} + moment-timezone@0.6.2: + resolution: {integrity: sha512-lDsQv8FoGdBUdf0+TjGsq2orxKuXdwFlQ6Zw6TX3xIcTwTfEpCLyKqvEauvCHJ8iu3KBV8+uPhlv70YsNGdUBQ==} - moment@2.29.4: - resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -6381,8 +6751,8 @@ packages: peerDependencies: msw: ^2.0.0 - msw@2.12.10: - resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} + msw@2.14.6: + resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -6399,9 +6769,9 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - mute-stream@2.0.0: - resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} - engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -6409,8 +6779,8 @@ packages: nanoclone@0.2.1: resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -6426,8 +6796,8 @@ packages: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true - needle@3.5.0: - resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} engines: {node: '>= 4.4.x'} hasBin: true @@ -6480,17 +6850,14 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - npm-run-path@5.1.0: resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6628,6 +6995,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.20.0: + resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -6751,10 +7125,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -6792,8 +7162,8 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} pkijs@3.3.3: resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} @@ -6803,47 +7173,47 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-calc@9.0.1: - resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-calc@10.1.1: + resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} + engines: {node: ^18.12 || ^20.9 || >=22.0} peerDependencies: - postcss: ^8.2.2 + postcss: ^8.4.38 - postcss-colormin@6.0.0: - resolution: {integrity: sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-colormin@8.0.0: + resolution: {integrity: sha512-KKwMmsSgsmdYXqrjQeqL3tnuIFtctiR1GEMHdjNpDpz/TCRkkkok2mMcreK2zVV3l7POWOmAkR2xYHUpRUK1DA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-convert-values@6.0.0: - resolution: {integrity: sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-convert-values@8.0.0: + resolution: {integrity: sha512-Ohtj3rNZWawTRePv5NCHTy8VJSdJ/G/uKuxcxJreOMichuqcT6uEl2TAnopVeJCJ/c13jaSqg7m63yFLM5zBsA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-discard-comments@6.0.0: - resolution: {integrity: sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-discard-comments@8.0.0: + resolution: {integrity: sha512-zGpvVLj2sbagEp+BTVETvAfkZdGVA6rALNujDK/WTIjdf1/rQOxOG8BBzkI8UQgnw8SkL6xffAfbtGMHFypadw==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-discard-duplicates@6.0.0: - resolution: {integrity: sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-discard-duplicates@8.0.0: + resolution: {integrity: sha512-zjRyYmNGI3PTipKBBtCgExlmZXQn49KvKoaiNnR2g+iXxeNk7GY5Js2ULtZXPrCYeqjPagrzKIBNcBocvXCR7g==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-discard-empty@6.0.0: - resolution: {integrity: sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-discard-empty@8.0.0: + resolution: {integrity: sha512-kxPJg6EqahbBvm+l7hpYYCtpsv8dlz7Tv6wJXUXZaeuY0WGS61DxfGdZR4uVB/Cx+yi3iOHQVSqpSHKMFaBg6Q==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-discard-overridden@6.0.0: - resolution: {integrity: sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-discard-overridden@8.0.0: + resolution: {integrity: sha512-sW2OWH3l9p0FmBSVr228uztFseqroZxwgD7SGF0Ks0dRPDttSo3P8FK5ZBLtWBH2A5+chpB0J2fB/T8heKHLBw==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} @@ -6876,41 +7246,41 @@ packages: postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 - postcss-merge-longhand@6.0.0: - resolution: {integrity: sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-merge-longhand@8.0.0: + resolution: {integrity: sha512-YDmAmQ8H+ljfomVpSXvr9NA0GP01fraQJqjWBYoMVGg6rOT+PJLwPyeVo2ekn4WB4ZVSH5ddtK3DTRxbz6CFzg==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-merge-rules@6.0.1: - resolution: {integrity: sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-merge-rules@8.0.0: + resolution: {integrity: sha512-bgstL5mpi41dDpnYGDUcI3M814NWkCMcIWpwDqEHXkHg3BT7b4XRAfNEuwJncZOVn/67kVKvWzhfv/7xyrp2uQ==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-minify-font-values@6.0.0: - resolution: {integrity: sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-minify-font-values@8.0.0: + resolution: {integrity: sha512-EnOHQEnSt6oH5NrL1DMFAQuwB2IOimFXTCzc9bKfUeH1jREbqIF5MAK4gQJQOC4mPUwJt4sWifAmNZ1qLu6j3Q==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-minify-gradients@6.0.0: - resolution: {integrity: sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-minify-gradients@8.0.0: + resolution: {integrity: sha512-43iAnYIGk0ZjNx5X/rkIcHi6dhmu/vEjY0kqfUfxPuJRO+V7jx8uKIdcnL0dpfNoC5J9TSh3EtzLWbq0gpqnWA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-minify-params@6.0.0: - resolution: {integrity: sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-minify-params@8.0.0: + resolution: {integrity: sha512-z7w4QO7G55l4vMUK1Lmx03GW7iyRLgf2V5Dz/7ioSPLnXRjeD+b7m0XfAXUGrbBYYrJ6bXPk+3LoX5u4JfAcSg==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-minify-selectors@6.0.0: - resolution: {integrity: sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-minify-selectors@8.0.1: + resolution: {integrity: sha512-c31D46811kTkQDxV1KTTow79axX6gj/01AY5G7cGZg3s31KvAwP13jEFXGAzQbJ7NvOFV1pRqEia6nrAdHU7qg==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 postcss-modules-extract-imports@3.0.0: resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} @@ -6960,77 +7330,77 @@ packages: peerDependencies: postcss: ^8.2.14 - postcss-normalize-charset@6.0.0: - resolution: {integrity: sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-charset@8.0.0: + resolution: {integrity: sha512-s88FUNDSUD8m0wBYvTQQcubVts6zhXwBU8zCD4vkRKiecd0v8cOjHVIF9r/i+5xzS/WG3f98qq4XsOM0JqvfLA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-display-values@6.0.0: - resolution: {integrity: sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-display-values@8.0.0: + resolution: {integrity: sha512-gG2nBxD27fiw6Luinb1QYKdM/Co5GornRJgSD+JTwNH4PGKxImP0qyruDDav49aHUPLY3qrL3qN3LvybO7IzxQ==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-positions@6.0.0: - resolution: {integrity: sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-positions@8.0.0: + resolution: {integrity: sha512-t/wGqpehS20Ke7kc4QAsWpH+AJjUdMK/V5qV2RhrXkj8hO/fT1t1MJ8NL7sedWYk7ZqC7eISEJQonW5j0tU1MQ==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-repeat-style@6.0.0: - resolution: {integrity: sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-repeat-style@8.0.0: + resolution: {integrity: sha512-3ebOmGdCYKrBYyGKc1xhj0unEnW7beZpVU7JohVeGl7mTxR+7T6egpaawTWAVsB0pEIhcsbJVOjPKCJSoRO6Zg==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-string@6.0.0: - resolution: {integrity: sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-string@8.0.0: + resolution: {integrity: sha512-TvWCGZ/e04Tv31uJvOUtbexkfgUnqmQ3M2P5DkAaVzvOj+BvTkG2QjpA5Y71SL1SPxJcj4M23fNh+RDVCmG8kA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-timing-functions@6.0.0: - resolution: {integrity: sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-timing-functions@8.0.0: + resolution: {integrity: sha512-uEfaXst5Xgqxv7geYUuz6vs9mn88K2NPY2RoIzM3BMmSjsdTSeppV9x2qIgrxsisdbSqF6IVhzI2occcte3hTA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-unicode@6.0.0: - resolution: {integrity: sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-unicode@8.0.0: + resolution: {integrity: sha512-+WYngZaChEeTHZmWhmKtnJ4gTzWdINEaFcgWBnu6WdVu8Ftim8OBTcw768DuCC/3Aax9bZ9WkwrLGHym2Lzf+A==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-url@6.0.0: - resolution: {integrity: sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-url@8.0.0: + resolution: {integrity: sha512-4Mz9hZHn/QIB+YtFqTXrDmE2193GYxGb3F8uMfLvMicaEXCCUlDIJ658gFFJbqEGl9FYzwPtRiuNgbwlO9kkBg==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-normalize-whitespace@6.0.0: - resolution: {integrity: sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-normalize-whitespace@8.0.0: + resolution: {integrity: sha512-V1f8tYnwIP5tscOXQFTKK8Y5EJ+R2GMpFJ6FjzwoKoQnhbqQy3IeSrDjJJb8JjVos8ut6Osi80Zybpayv/XjIQ==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-ordered-values@6.0.0: - resolution: {integrity: sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-ordered-values@8.0.0: + resolution: {integrity: sha512-Dg9+itb6lmD0bxqhQyHCtXAwYRh0wUrx6Mp4/BNXgkLoJmdYMmWi+V+Pypw79Q6iQhxA8KFMHqLBITQJV2gKMA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-reduce-initial@6.0.0: - resolution: {integrity: sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-reduce-initial@8.0.0: + resolution: {integrity: sha512-DChcE9d528AKrlpCTHjhsAiOsWCk4H9ApHPS1QqRT3praObWTiWyn6W1UddGpc46K9LQnHwUu4YwaPUukGtXVA==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-reduce-transforms@6.0.0: - resolution: {integrity: sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-reduce-transforms@8.0.0: + resolution: {integrity: sha512-cLZT0som7vvumQT9XQCnSKOSnRinNQZd1Hm+J723Ney13E8CIydDhw6JwzsjPtgnYThTqn9Q45906gz6wxaAsw==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 postcss-selector-parser@6.0.13: resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} @@ -7040,27 +7410,23 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss-svgo@6.0.0: - resolution: {integrity: sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==} - engines: {node: ^14 || ^16 || >= 18} + postcss-svgo@8.0.0: + resolution: {integrity: sha512-Q2fMSYEiNE1ioDc/3sxvI24NdgA/MJno2XLNpOxgv8aCcJbym8mZY10/lDY5+AWCIc3Aiqzy2Wcp9/zaIXBZgQ==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 - postcss-unique-selectors@6.0.0: - resolution: {integrity: sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==} - engines: {node: ^14 || ^16 || >=18.0} + postcss-unique-selectors@8.0.0: + resolution: {integrity: sha512-iObuolUX+ITJfMU2QQFQdh31JgSjNLPNjVs6YGAqBHvOvAWXMMNget6donQl83aQaeS32i5XeKZURUW/WBxIUw==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: @@ -7154,8 +7520,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -7348,8 +7715,8 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-json-view-lite@1.2.1: - resolution: {integrity: sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ==} + react-json-view-lite@1.5.0: + resolution: {integrity: sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==} engines: {node: '>=14'} peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -7380,11 +7747,11 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-select: ^5.0.0 - react-select@5.2.1: - resolution: {integrity: sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==} + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} peerDependencies: - react: ^16.8.0 || ^17.0.0 - react-dom: ^16.8.0 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} @@ -7566,8 +7933,8 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} - rettime@0.10.1: - resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -7586,8 +7953,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7700,9 +8067,6 @@ packages: serialize-javascript@6.0.1: resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} engines: {node: '>= 0.8.0'} @@ -7714,6 +8078,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -7843,6 +8210,10 @@ packages: source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -7880,10 +8251,6 @@ packages: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -7902,14 +8269,20 @@ packages: storybook-css-modules-preset@1.1.1: resolution: {integrity: sha512-wyINPOtB/8SvU7J92ePAhciBk4xoHuwrcqDNyGnSwilwHjmtHwbeEgZ1//JALOTwMB10zwz3WPONRkWec9LdGw==} - storybook@10.3.5: - resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==} + storybook@10.4.2: + resolution: {integrity: sha512-5Ax5vbHxFgMBGGhQDm75Rrumm/HZC4ICFhMcJaM0UlqnC/4FKj/IaZtImZFupknyiiyUEcWHPQFA2kX3/VSv1A==} hasBin: true peerDependencies: + '@types/react': ^17.0.37 prettier: ^2 || ^3 + vite-plus: ^0.1.15 peerDependenciesMeta: + '@types/react': + optional: true prettier: optional: true + vite-plus: + optional: true strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -8011,15 +8384,18 @@ packages: style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} - stylehacks@6.0.0: - resolution: {integrity: sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==} - engines: {node: ^14 || ^16 || >=18.0} + stylehacks@8.0.0: + resolution: {integrity: sha512-sWyjaJvBqHoVKYPbQ8JRvrGSPaYWtWrJsU+fGVtwKB1GE1rRPu3rC7T6UCuXLoL00Dwb+tsHe2T904r8Vnsx8w==} + engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.2.15 + postcss: ^8.5.14 stylis@4.0.10: resolution: {integrity: sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==} + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + sucrase@3.34.0: resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} engines: {node: '>=8'} @@ -8053,6 +8429,11 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + swagger2openapi@7.0.8: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true @@ -8090,19 +8471,50 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} engines: {node: '>= 10.13.0'} peerDependencies: + '@minify-html/node': '*' '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' uglify-js: '*' webpack: ^5.1.0 peerDependenciesMeta: + '@minify-html/node': + optional: true '@swc/core': optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true esbuild: optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true uglify-js: optional: true @@ -8148,6 +8560,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -8196,8 +8612,8 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} tr46@0.0.3: @@ -8273,8 +8689,8 @@ packages: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} - type-fest@5.4.4: - resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + type-fest@5.7.0: + resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} type-is@1.6.18: @@ -8312,8 +8728,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.58.2: - resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -8481,6 +8897,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.2.0: @@ -8520,13 +8937,13 @@ packages: vite: optional: true - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^25 - '@vitejs/devtools': ^0.1.0 + '@vitejs/devtools': ^0.1.18 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -8674,8 +9091,8 @@ packages: webpack: optional: true - webpack-dev-server@5.2.3: - resolution: {integrity: sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==} + webpack-dev-server@5.2.4: + resolution: {integrity: sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==} engines: {node: '>= 18.12.0'} hasBin: true peerDependencies: @@ -8694,18 +9111,15 @@ packages: resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} engines: {node: '>=18.0.0'} - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} engines: {node: '>=10.13.0'} - webpack-virtual-modules@0.6.1: - resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.105.0: - resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} + webpack@5.107.2: + resolution: {integrity: sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -8884,10 +9298,6 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} - yoctocolors-cjs@2.1.3: - resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} - engines: {node: '>=18'} - yup@0.32.11: resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} engines: {node: '>=10'} @@ -9078,7 +9488,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -9867,109 +10277,123 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@chromatic-com/storybook@5.1.2(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': + '@chromatic-com/storybook@5.1.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 13.3.5 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) strip-ansi: 7.1.0 transitivePeerDependencies: - '@chromatic-com/cypress' - '@chromatic-com/playwright' - '@codemirror/autocomplete@6.18.6': + '@codemirror/autocomplete@6.20.2': dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 '@lezer/common': 1.2.3 - '@codemirror/commands@6.8.0': + '@codemirror/commands@6.10.3': dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 '@lezer/common': 1.2.3 '@codemirror/lang-json@6.0.1': dependencies: - '@codemirror/language': 6.10.8 + '@codemirror/language': 6.12.3 '@lezer/json': 1.0.3 optional: true '@codemirror/lang-yaml@6.1.2': dependencies: - '@codemirror/autocomplete': 6.18.6 - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 '@lezer/yaml': 1.0.3 optional: true - '@codemirror/language@6.10.8': + '@codemirror/language@6.12.3': dependencies: - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 - '@lezer/common': 1.2.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 style-mod: 4.1.2 - '@codemirror/legacy-modes@6.4.2': + '@codemirror/legacy-modes@6.5.3': dependencies: - '@codemirror/language': 6.10.8 + '@codemirror/language': 6.12.3 - '@codemirror/lint@6.8.4': + '@codemirror/lint@6.9.6': dependencies: - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 crelt: 1.0.6 '@codemirror/merge@6.10.0': dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 '@lezer/highlight': 1.2.1 style-mod: 4.1.2 - '@codemirror/search@6.5.8': + '@codemirror/search@6.7.0': dependencies: - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 crelt: 1.0.6 - '@codemirror/state@6.5.1': + '@codemirror/state@6.6.0': dependencies: '@marijn/find-cluster-break': 1.0.2 - '@codemirror/theme-one-dark@6.1.2': + '@codemirror/theme-one-dark@6.1.3': dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 '@lezer/highlight': 1.2.1 - '@codemirror/view@6.36.2': + '@codemirror/view@6.43.0': dependencies: - '@codemirror/state': 6.5.1 + '@codemirror/state': 6.6.0 + crelt: 1.0.6 style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@colordx/core@5.4.3': {} + '@discoveryjs/json-ext@0.5.7': {} '@discoveryjs/json-ext@0.6.3': {} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -9980,6 +10404,30 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.26.0 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + '@emotion/cache@11.6.0': dependencies: '@emotion/memoize': 0.7.5 @@ -9988,40 +10436,54 @@ snapshots: '@emotion/weak-memoize': 0.2.5 stylis: 4.0.10 - '@emotion/hash@0.8.0': {} + '@emotion/hash@0.9.2': {} '@emotion/memoize@0.7.5': {} - '@emotion/react@11.6.0(@babel/core@7.23.6)(@types/react@17.0.75)(react@17.0.2)': + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@17.0.75)(react@17.0.2)': dependencies: '@babel/runtime': 7.26.0 - '@emotion/cache': 11.6.0 - '@emotion/serialize': 1.0.2 - '@emotion/sheet': 1.1.0 - '@emotion/utils': 1.0.0 - '@emotion/weak-memoize': 0.2.5 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@17.0.2) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 react: 17.0.2 optionalDependencies: - '@babel/core': 7.23.6 '@types/react': 17.0.75 + transitivePeerDependencies: + - supports-color - '@emotion/serialize@1.0.2': + '@emotion/serialize@1.3.3': dependencies: - '@emotion/hash': 0.8.0 - '@emotion/memoize': 0.7.5 - '@emotion/unitless': 0.7.5 - '@emotion/utils': 1.0.0 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 csstype: 3.0.10 '@emotion/sheet@1.1.0': {} - '@emotion/unitless@0.7.5': {} + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@17.0.2)': + dependencies: + react: 17.0.2 '@emotion/utils@1.0.0': {} + '@emotion/utils@1.4.2': {} + '@emotion/weak-memoize@0.2.5': {} + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -10100,24 +10562,24 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@9.39.4(jiti@2.6.1))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@9.39.4(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.0.5(eslint@9.39.4(jiti@2.6.1))': + '@eslint/compat@2.0.5(eslint@9.39.4(jiti@2.7.0))': dependencies: '@eslint/core': 1.2.1 optionalDependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) '@eslint/config-array@0.21.2': dependencies: @@ -10164,6 +10626,17 @@ snapshots: '@exodus/schemasafe@1.0.0-rc.6': {} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + '@hey-api/codegen-core@0.8.2(magicast@0.5.2)': dependencies: '@hey-api/types': 0.1.4 @@ -10225,31 +10698,30 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@1.0.2': {} + '@inquirer/ansi@2.0.7': {} - '@inquirer/confirm@5.1.21(@types/node@25.0.3)': + '@inquirer/confirm@6.1.1(@types/node@25.0.3)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.3) - '@inquirer/type': 3.0.10(@types/node@25.0.3) + '@inquirer/core': 11.2.1(@types/node@25.0.3) + '@inquirer/type': 4.0.7(@types/node@25.0.3) optionalDependencies: '@types/node': 25.0.3 - '@inquirer/core@10.3.2(@types/node@25.0.3)': + '@inquirer/core@11.2.1(@types/node@25.0.3)': dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.3) + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@25.0.3) cli-width: 4.1.0 - mute-stream: 2.0.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 optionalDependencies: '@types/node': 25.0.3 - '@inquirer/figures@1.0.15': {} + '@inquirer/figures@2.0.7': {} - '@inquirer/type@3.0.10(@types/node@25.0.3)': + '@inquirer/type@4.0.7(@types/node@25.0.3)': optionalDependencies: '@types/node': 25.0.3 @@ -10433,6 +10905,8 @@ snapshots: '@lezer/common@1.2.3': {} + '@lezer/common@1.5.2': {} + '@lezer/highlight@1.2.1': dependencies: '@lezer/common': 1.2.3 @@ -10446,7 +10920,7 @@ snapshots: '@lezer/lr@1.4.2': dependencies: - '@lezer/common': 1.2.3 + '@lezer/common': 1.5.2 '@lezer/yaml@1.0.3': dependencies: @@ -10465,7 +10939,7 @@ snapshots: '@types/react': 17.0.75 react: 17.0.2 - '@mswjs/interceptors@0.41.2': + '@mswjs/interceptors@0.41.9': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -10481,7 +10955,14 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 @@ -10508,6 +10989,8 @@ snapshots: '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 @@ -10515,7 +10998,134 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-project/types@0.124.0': {} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-project/types@0.133.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.20.0': + optional: true + + '@oxc-resolver/binding-android-arm64@11.20.0': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.20.0': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.20.0': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.20.0': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.20.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.20.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.20.0': + optional: true '@peculiar/asn1-cms@2.6.0': dependencies: @@ -10916,62 +11526,62 @@ snapshots: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@rolldown/binding-android-arm64@1.0.0-rc.15': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@5.3.0(rollup@4.54.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.54.0 @@ -11098,97 +11708,109 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.5(@types/react@17.0.75)(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.105.0)': + '@storybook/addon-docs@10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.107.2)': dependencies: '@mdx-js/react': 3.1.1(@types/react@17.0.75)(react@17.0.2) - '@storybook/csf-plugin': 10.3.5(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.105.0) - '@storybook/icons': 2.0.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - '@storybook/react-dom-shim': 10.3.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + '@storybook/csf-plugin': 10.4.2(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.107.2) + '@storybook/icons': 2.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@storybook/react-dom-shim': 10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) ts-dedent: 2.2.0 + optionalDependencies: + '@types/react': 17.0.75 transitivePeerDependencies: - - '@types/react' + - '@types/react-dom' - esbuild - rollup - vite - webpack - '@storybook/addon-links@10.3.5(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': + '@storybook/addon-links@10.4.2(@types/react@17.0.75)(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) optionalDependencies: + '@types/react': 17.0.75 react: 17.0.2 - '@storybook/addon-styling-webpack@3.0.2(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.105.0)': + '@storybook/addon-styling-webpack@3.0.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.107.2)': dependencies: - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) - '@storybook/addon-webpack5-compiler-swc@4.0.3(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.105.0)': + '@storybook/addon-webpack5-compiler-swc@4.0.3(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(webpack@5.107.2)': dependencies: '@swc/core': 1.15.11 - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - swc-loader: 0.2.7(@swc/core@1.15.11)(webpack@5.105.0) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + swc-loader: 0.2.7(@swc/core@1.15.11)(webpack@5.107.2) transitivePeerDependencies: - '@swc/helpers' - webpack - '@storybook/builder-webpack5@10.3.5(@swc/core@1.15.11)(esbuild@0.27.3)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1)': + '@storybook/builder-webpack5@10.4.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1)': dependencies: - '@storybook/core-webpack': 10.3.5(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + '@storybook/core-webpack': 10.4.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 - css-loader: 7.1.4(webpack@5.105.0) + css-loader: 7.1.4(webpack@5.107.2) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@6.0.2)(webpack@5.105.0) - html-webpack-plugin: 5.5.3(webpack@5.105.0) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@6.0.2)(webpack@5.107.2) + html-webpack-plugin: 5.5.3(webpack@5.107.2) magic-string: 0.30.21 - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) - style-loader: 4.0.0(webpack@5.105.0) - terser-webpack-plugin: 5.3.16(@swc/core@1.15.11)(esbuild@0.27.3)(webpack@5.105.0) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + style-loader: 4.0.0(webpack@5.107.2) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack@5.107.2) ts-dedent: 2.2.0 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) - webpack-dev-middleware: 6.1.3(webpack@5.105.0) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) + webpack-dev-middleware: 6.1.3(webpack@5.107.2) webpack-hot-middleware: 2.25.3 - webpack-virtual-modules: 0.6.1 + webpack-virtual-modules: 0.6.2 optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: + - '@minify-html/node' - '@rspack/core' - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - uglify-js - webpack-cli - '@storybook/core-webpack@10.3.5(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': + '@storybook/core-webpack@10.4.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': dependencies: - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) ts-dedent: 2.2.0 - '@storybook/csf-plugin@10.3.5(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.105.0)': + '@storybook/csf-plugin@10.4.2(esbuild@0.27.3)(rollup@4.54.0)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))(webpack@5.107.2)': dependencies: - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 rollup: 4.54.0 - vite: 8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + vite: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) '@storybook/global@5.0.0': {} - '@storybook/icons@2.0.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@storybook/icons@2.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@storybook/preset-react-webpack@10.3.5(@swc/core@1.15.11)(esbuild@0.27.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1)': + '@storybook/preset-react-webpack@10.4.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1)': dependencies: - '@storybook/core-webpack': 10.3.5(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@6.0.2)(webpack@5.105.0) + '@storybook/core-webpack': 10.4.2(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@6.0.2)(webpack@5.107.2) '@types/semver': 7.7.1 magic-string: 0.30.21 react: 17.0.2 @@ -11196,19 +11818,28 @@ snapshots: react-dom: 17.0.2(react@17.0.2) resolve: 1.22.11 semver: 7.7.3 - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) tsconfig-paths: 4.2.0 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: + - '@minify-html/node' - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - supports-color - uglify-js - webpack-cli - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@6.0.2)(webpack@5.105.0)': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@6.0.2)(webpack@5.107.2)': dependencies: debug: 4.4.3 endent: 2.1.0 @@ -11218,44 +11849,60 @@ snapshots: react-docgen-typescript: 2.2.2(typescript@6.0.2) tslib: 2.8.1 typescript: 6.0.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@10.3.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': + '@storybook/react-dom-shim@10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))': dependencies: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + optionalDependencies: + '@types/react': 17.0.75 + '@types/react-dom': 17.0.25 - '@storybook/react-webpack5@10.3.5(@swc/core@1.15.11)(esbuild@0.27.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1)': + '@storybook/react-webpack5@10.4.2(@swc/core@1.15.11)(@types/react-dom@17.0.25)(@types/react@17.0.75)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1)': dependencies: - '@storybook/builder-webpack5': 10.3.5(@swc/core@1.15.11)(esbuild@0.27.3)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1) - '@storybook/preset-react-webpack': 10.3.5(@swc/core@1.15.11)(esbuild@0.27.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1) - '@storybook/react': 10.3.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2) + '@storybook/builder-webpack5': 10.4.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1) + '@storybook/preset-react-webpack': 10.4.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)(webpack-cli@6.0.1) + '@storybook/react': 10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2) react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: + - '@minify-html/node' - '@rspack/core' - '@swc/core' + - '@swc/css' + - '@swc/html' + - '@types/react' + - '@types/react-dom' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - supports-color - uglify-js - webpack-cli - '@storybook/react@10.3.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)': + '@storybook/react@10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.3.5(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) + '@storybook/react-dom-shim': 10.4.2(@types/react-dom@17.0.25)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)) react: 17.0.2 react-docgen: 8.0.3 react-docgen-typescript: 2.2.2(typescript@6.0.2) react-dom: 17.0.2(react@17.0.2) - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) optionalDependencies: + '@types/react': 17.0.75 + '@types/react-dom': 17.0.25 typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -11480,10 +12127,6 @@ snapshots: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - '@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4)': - dependencies: - '@testing-library/dom': 9.3.4 - '@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)': dependencies: '@testing-library/dom': 9.3.4 @@ -11524,10 +12167,6 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 - '@types/babel__traverse@7.20.4': - dependencies: - '@babel/types': 7.29.0 - '@types/babel__traverse@7.28.0': dependencies: '@babel/types': 7.29.0 @@ -11559,20 +12198,8 @@ snapshots: '@types/doctrine@0.0.9': {} - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 - - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@types/estree@1.0.8': {} - '@types/estree@1.0.9': {} - '@types/express-serve-static-core@4.17.34': dependencies: '@types/node': 25.0.3 @@ -11604,6 +12231,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.7(@types/react@17.0.75)': + dependencies: + '@types/react': 17.0.75 + hoist-non-react-statics: 3.3.2 + '@types/html-minifier-terser@6.1.0': {} '@types/http-errors@2.0.5': {} @@ -11632,9 +12264,9 @@ snapshots: '@types/lodash.memoize@4.1.9': dependencies: - '@types/lodash': 4.17.21 + '@types/lodash': 4.17.24 - '@types/lodash@4.17.21': {} + '@types/lodash@4.17.24': {} '@types/mdast@4.0.4': dependencies: @@ -11654,6 +12286,8 @@ snapshots: '@types/nprogress@0.2.3': {} + '@types/parse-json@4.0.2': {} + '@types/prop-types@15.7.4': {} '@types/qs@6.14.0': {} @@ -11713,6 +12347,10 @@ snapshots: '@types/node': 25.0.3 '@types/send': 0.17.6 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 25.0.3 + '@types/sizzle@2.3.10': {} '@types/sockjs@0.3.36': @@ -11739,15 +12377,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.0 - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 9.39.4(jiti@2.6.1) + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.2) @@ -11755,56 +12393,91 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': + '@typescript-eslint/project-service@8.58.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 debug: 4.4.3 typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.58.2': + '@typescript-eslint/project-service@8.60.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.2) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': + '@typescript-eslint/scope-manager@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + + '@typescript-eslint/scope-manager@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.2)': dependencies: typescript: 6.0.2 - '@typescript-eslint/type-utils@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.58.1': {} - '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': + '@typescript-eslint/types@8.60.1': {} + + '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/project-service': 8.58.1(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.2) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.3 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.2) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.3 @@ -11814,20 +12487,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': + '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - eslint: 9.39.4(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.2) + eslint: 9.39.4(jiti@2.7.0) typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.58.2': + '@typescript-eslint/utils@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)': dependencies: - '@typescript-eslint/types': 8.58.2 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.2) + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + eslint-visitor-keys: 5.0.1 + + '@typescript-eslint/visitor-keys@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 eslint-visitor-keys: 5.0.1 '@uirouter/angularjs@1.0.11(angular@1.8.2)': @@ -11854,30 +12543,57 @@ snapshots: prop-types: 15.8.1 react: 17.0.2 - '@uiw/codemirror-extensions-basic-setup@4.23.11(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': + '@uiw/codemirror-extensions-basic-setup@4.23.11(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': dependencies: - '@codemirror/autocomplete': 6.18.6 - '@codemirror/commands': 6.8.0 - '@codemirror/language': 6.10.8 - '@codemirror/lint': 6.8.4 - '@codemirror/search': 6.5.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 - '@uiw/codemirror-themes@4.23.7(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': + '@uiw/codemirror-extensions-basic-setup@4.25.10(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 - '@uiw/react-codemirror@4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.2)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + '@uiw/codemirror-themes@4.25.10(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + + '@uiw/react-codemirror@4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@babel/runtime': 7.26.0 - '@codemirror/commands': 6.8.0 - '@codemirror/state': 6.5.1 - '@codemirror/theme-one-dark': 6.1.2 - '@codemirror/view': 6.36.2 - '@uiw/codemirror-extensions-basic-setup': 4.23.11(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) + '@codemirror/commands': 6.10.3 + '@codemirror/state': 6.6.0 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.0 + '@uiw/codemirror-extensions-basic-setup': 4.23.11(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) + codemirror: 6.0.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + + '@uiw/react-codemirror@4.25.10(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.26.0 + '@codemirror/commands': 6.10.3 + '@codemirror/state': 6.6.0 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.0 + '@uiw/codemirror-extensions-basic-setup': 4.25.10(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) codemirror: 6.0.1 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) @@ -11960,17 +12676,17 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) + vitest: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) - '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)(vitest@4.1.8)': + '@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2)(vitest@4.1.8)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - eslint: 9.39.4(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + eslint: 9.39.4(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) typescript: 6.0.2 - vitest: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) + vitest: 4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) transitivePeerDependencies: - supports-color @@ -11991,14 +12707,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))': + '@vitest/mocker@4.1.8(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.12.10(@types/node@25.0.3)(typescript@6.0.2) - vite: 8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) + msw: 2.14.6(@types/node@25.0.3)(typescript@6.0.2) + vite: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -12120,22 +12836,22 @@ snapshots: '@webcontainer/env@1.1.1': {} - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.0)': + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.107.2)': dependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2) - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.0)': + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.107.2)': dependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.105.0)': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.4)(webpack@5.107.2)': dependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2) optionalDependencies: - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.105.0) + webpack-dev-server: 5.2.4(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.107.2) '@wojtekmaj/date-utils@1.5.1': {} @@ -12169,9 +12885,9 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-phases@1.0.4(acorn@8.15.0): + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: @@ -12181,6 +12897,14 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + agent-base@7.1.4: {} ajv-formats@2.1.1(ajv@8.17.1): @@ -12429,24 +13153,23 @@ snapshots: asynckit@0.4.0: {} - auto-ngtemplate-loader@3.1.2(webpack@5.105.0): + auto-ngtemplate-loader@3.1.2(webpack@5.107.2): dependencies: - html-loader: 4.1.0(webpack@5.105.0) + html-loader: 4.1.0(webpack@5.107.2) loader-utils: 3.2.2 - lodash: 4.17.21 + lodash: 4.17.23 ngtemplate-loader: 2.1.0 var-validator: 0.0.3 transitivePeerDependencies: - webpack - autoprefixer@10.4.16(postcss@8.5.6): + autoprefixer@10.5.0(postcss@8.5.15): dependencies: - browserslist: 4.22.2 - caniuse-lite: 1.0.30001751 - fraction.js: 4.3.6 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.5.6 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.15 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.5: {} @@ -12457,35 +13180,37 @@ snapshots: axe-core@4.11.2: {} - axios-cache-interceptor@1.11.2(axios@1.13.5): + axios-cache-interceptor@1.11.2(axios@1.16.1): dependencies: - axios: 1.13.5 + axios: 1.16.1 cache-parser: 1.2.6 fast-defer: 1.1.9 http-vary: 1.0.3 object-code: 2.0.0 try: 1.0.3 - axios-progress-bar@https://codeload.github.com/portainer/progress-bar-4-axios/tar.gz/d424cd29f5b629af9ffc5c3d6db8d92d11d82f0f(axios@1.13.5): + axios-progress-bar@https://codeload.github.com/portainer/progress-bar-4-axios/tar.gz/d424cd29f5b629af9ffc5c3d6db8d92d11d82f0f(axios@1.16.1): dependencies: - axios: 1.13.5 + axios: 1.16.1 - axios@1.13.5: + axios@1.16.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 form-data: 4.0.5 - proxy-from-env: 1.1.0 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color axobject-query@4.1.0: {} - babel-loader@9.1.3(@babel/core@7.23.6)(webpack@5.105.0): + babel-loader@9.1.3(@babel/core@7.23.6)(webpack@5.107.2): dependencies: '@babel/core': 7.23.6 find-cache-dir: 4.0.0 schema-utils: 4.0.0 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) babel-plugin-angularjs-annotate@0.10.0: dependencies: @@ -12508,9 +13233,15 @@ snapshots: '@babel/helper-module-imports': 7.22.15 '@babel/types': 7.26.3 glob: 7.2.3 - lodash: 4.17.21 + lodash: 4.18.1 require-package-name: 2.0.1 + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.26.0 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + babel-plugin-polyfill-corejs2@0.4.7(@babel/core@7.23.6): dependencies: '@babel/compat-data': 7.23.5 @@ -12541,7 +13272,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.9.11: {} + baseline-browser-mapping@2.10.33: {} batch@0.6.1: {} @@ -12599,13 +13330,13 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.9.11 - caniuse-lite: 1.0.30001762 - electron-to-chromium: 1.5.267 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer-from@1.1.2: {} @@ -12630,11 +13361,11 @@ snapshots: dotenv: 17.4.2 exsolve: 1.0.8 giget: 3.2.0 - jiti: 2.6.1 + jiti: 2.7.0 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.1.0 - pkg-types: 2.3.0 + pkg-types: 2.3.1 rc9: 3.0.1 optionalDependencies: magicast: 0.5.2 @@ -12694,14 +13425,14 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001751 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 caniuse-lite@1.0.30001751: {} - caniuse-lite@1.0.30001762: {} + caniuse-lite@1.0.30001793: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -12739,7 +13470,7 @@ snapshots: chart.js@2.9.4: dependencies: chartjs-color: 2.4.1 - moment: 2.29.4 + moment: 2.30.1 chartjs-color-string@0.6.0: dependencies: @@ -12800,10 +13531,10 @@ snapshots: dependencies: source-map: 0.6.1 - clean-webpack-plugin@4.0.0(webpack@5.105.0): + clean-webpack-plugin@4.0.0(webpack@5.107.2): dependencies: del: 4.1.1 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) cli-cursor@4.0.0: dependencies: @@ -12838,12 +13569,12 @@ snapshots: clsx@2.1.1: {} - codemirror-json-schema@0.8.0(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)(@lezer/common@1.2.3): + codemirror-json-schema@0.8.1(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.2.3): dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/lint': 6.8.4 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 '@lezer/common': 1.2.3 '@sagold/json-pointer': 5.1.2 '@shikijs/markdown-it': 1.29.2 @@ -12855,7 +13586,7 @@ snapshots: shiki: 1.29.2 yaml: 2.7.0 optionalDependencies: - '@codemirror/autocomplete': 6.18.6 + '@codemirror/autocomplete': 6.20.2 '@codemirror/lang-json': 6.0.1 '@codemirror/lang-yaml': 6.1.2 codemirror-json5: 1.0.3 @@ -12863,9 +13594,9 @@ snapshots: codemirror-json5@1.0.3: dependencies: - '@codemirror/language': 6.10.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 '@lezer/common': 1.2.3 '@lezer/highlight': 1.2.1 json5: 2.2.3 @@ -12874,13 +13605,13 @@ snapshots: codemirror@6.0.1: dependencies: - '@codemirror/autocomplete': 6.18.6 - '@codemirror/commands': 6.8.0 - '@codemirror/language': 6.10.8 - '@codemirror/lint': 6.8.4 - '@codemirror/search': 6.5.8 - '@codemirror/state': 6.5.1 - '@codemirror/view': 6.36.2 + '@codemirror/autocomplete': 6.20.2 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.6 + '@codemirror/search': 6.7.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 color-convert@1.9.3: dependencies: @@ -12896,8 +13627,6 @@ snapshots: color-support@1.1.3: {} - colord@2.9.2: {} - colorette@2.0.20: {} combined-stream@1.0.8: @@ -12910,6 +13639,8 @@ snapshots: commander@11.0.0: {} + commander@11.1.0: {} + commander@12.1.0: {} commander@14.0.3: {} @@ -12954,6 +13685,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} @@ -12971,7 +13704,7 @@ snapshots: dependencies: is-what: 4.1.16 - copy-webpack-plugin@11.0.0(webpack@5.105.0): + copy-webpack-plugin@11.0.0(webpack@5.107.2): dependencies: fast-glob: 3.3.2 glob-parent: 6.0.2 @@ -12979,7 +13712,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.0.0 serialize-javascript: 6.0.1 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) core-js-compat@3.34.0: dependencies: @@ -12989,6 +13722,14 @@ snapshots: core-util-is@1.0.3: {} + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + cosmiconfig@8.2.0: dependencies: import-fresh: 3.3.0 @@ -13010,34 +13751,30 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@6.4.0(postcss@8.5.6): + css-loader@6.8.1(webpack@5.107.2): dependencies: - postcss: 8.5.6 - - css-loader@6.8.1(webpack@5.105.0): - dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-modules-extract-imports: 3.0.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.0.3(postcss@8.5.6) - postcss-modules-scope: 3.0.0(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-modules-extract-imports: 3.0.0(postcss@8.5.15) + postcss-modules-local-by-default: 4.0.3(postcss@8.5.15) + postcss-modules-scope: 3.0.0(postcss@8.5.15) + postcss-modules-values: 4.0.0(postcss@8.5.15) postcss-value-parser: 4.2.0 semver: 7.5.4 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) - css-loader@7.1.4(webpack@5.105.0): + css-loader@7.1.4(webpack@5.107.2): dependencies: - icss-utils: 5.1.0(postcss@8.5.9) - postcss: 8.5.9 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.9) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.9) - postcss-modules-scope: 3.2.1(postcss@8.5.9) - postcss-modules-values: 4.0.0(postcss@8.5.9) + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.15) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.15) + postcss-modules-scope: 3.2.1(postcss@8.5.15) + postcss-modules-values: 4.0.0(postcss@8.5.15) postcss-value-parser: 4.2.0 semver: 7.7.3 optionalDependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) css-select@4.3.0: dependencies: @@ -13065,54 +13802,59 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.1.0: {} css.escape@1.5.1: {} cssesc@3.0.0: {} - cssnano-preset-default@6.0.1(postcss@8.5.6): + cssnano-preset-default@8.0.1(postcss@8.5.15): dependencies: - css-declaration-sorter: 6.4.0(postcss@8.5.6) - cssnano-utils: 4.0.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-calc: 9.0.1(postcss@8.5.6) - postcss-colormin: 6.0.0(postcss@8.5.6) - postcss-convert-values: 6.0.0(postcss@8.5.6) - postcss-discard-comments: 6.0.0(postcss@8.5.6) - postcss-discard-duplicates: 6.0.0(postcss@8.5.6) - postcss-discard-empty: 6.0.0(postcss@8.5.6) - postcss-discard-overridden: 6.0.0(postcss@8.5.6) - postcss-merge-longhand: 6.0.0(postcss@8.5.6) - postcss-merge-rules: 6.0.1(postcss@8.5.6) - postcss-minify-font-values: 6.0.0(postcss@8.5.6) - postcss-minify-gradients: 6.0.0(postcss@8.5.6) - postcss-minify-params: 6.0.0(postcss@8.5.6) - postcss-minify-selectors: 6.0.0(postcss@8.5.6) - postcss-normalize-charset: 6.0.0(postcss@8.5.6) - postcss-normalize-display-values: 6.0.0(postcss@8.5.6) - postcss-normalize-positions: 6.0.0(postcss@8.5.6) - postcss-normalize-repeat-style: 6.0.0(postcss@8.5.6) - postcss-normalize-string: 6.0.0(postcss@8.5.6) - postcss-normalize-timing-functions: 6.0.0(postcss@8.5.6) - postcss-normalize-unicode: 6.0.0(postcss@8.5.6) - postcss-normalize-url: 6.0.0(postcss@8.5.6) - postcss-normalize-whitespace: 6.0.0(postcss@8.5.6) - postcss-ordered-values: 6.0.0(postcss@8.5.6) - postcss-reduce-initial: 6.0.0(postcss@8.5.6) - postcss-reduce-transforms: 6.0.0(postcss@8.5.6) - postcss-svgo: 6.0.0(postcss@8.5.6) - postcss-unique-selectors: 6.0.0(postcss@8.5.6) + browserslist: 4.28.2 + cssnano-utils: 6.0.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-calc: 10.1.1(postcss@8.5.15) + postcss-colormin: 8.0.0(postcss@8.5.15) + postcss-convert-values: 8.0.0(postcss@8.5.15) + postcss-discard-comments: 8.0.0(postcss@8.5.15) + postcss-discard-duplicates: 8.0.0(postcss@8.5.15) + postcss-discard-empty: 8.0.0(postcss@8.5.15) + postcss-discard-overridden: 8.0.0(postcss@8.5.15) + postcss-merge-longhand: 8.0.0(postcss@8.5.15) + postcss-merge-rules: 8.0.0(postcss@8.5.15) + postcss-minify-font-values: 8.0.0(postcss@8.5.15) + postcss-minify-gradients: 8.0.0(postcss@8.5.15) + postcss-minify-params: 8.0.0(postcss@8.5.15) + postcss-minify-selectors: 8.0.1(postcss@8.5.15) + postcss-normalize-charset: 8.0.0(postcss@8.5.15) + postcss-normalize-display-values: 8.0.0(postcss@8.5.15) + postcss-normalize-positions: 8.0.0(postcss@8.5.15) + postcss-normalize-repeat-style: 8.0.0(postcss@8.5.15) + postcss-normalize-string: 8.0.0(postcss@8.5.15) + postcss-normalize-timing-functions: 8.0.0(postcss@8.5.15) + postcss-normalize-unicode: 8.0.0(postcss@8.5.15) + postcss-normalize-url: 8.0.0(postcss@8.5.15) + postcss-normalize-whitespace: 8.0.0(postcss@8.5.15) + postcss-ordered-values: 8.0.0(postcss@8.5.15) + postcss-reduce-initial: 8.0.0(postcss@8.5.15) + postcss-reduce-transforms: 8.0.0(postcss@8.5.15) + postcss-svgo: 8.0.0(postcss@8.5.15) + postcss-unique-selectors: 8.0.0(postcss@8.5.15) - cssnano-utils@4.0.0(postcss@8.5.6): + cssnano-utils@6.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 - cssnano@6.0.1(postcss@8.5.6): + cssnano@8.0.1(postcss@8.5.15): dependencies: - cssnano-preset-default: 6.0.1(postcss@8.5.6) - lilconfig: 2.1.0 - postcss: 8.5.6 + cssnano-preset-default: 8.0.1(postcss@8.5.15) + lilconfig: 3.1.3 + postcss: 8.5.15 csso@5.0.5: dependencies: @@ -13351,10 +14093,10 @@ snapshots: dependencies: dotenv: 8.6.0 - dotenv-webpack@8.0.1(webpack@5.105.0): + dotenv-webpack@8.0.1(webpack@5.107.2): dependencies: dotenv-defaults: 2.0.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) dotenv@17.4.2: {} @@ -13374,7 +14116,7 @@ snapshots: electron-to-chromium@1.4.616: {} - electron-to-chromium@1.5.267: {} + electron-to-chromium@1.5.364: {} emoji-regex-xs@1.0.0: {} @@ -13392,10 +14134,10 @@ snapshots: fast-json-parse: 1.0.3 objectorarray: 1.0.5 - enhanced-resolve@5.19.0: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.3 entities@2.1.0: {} @@ -13616,8 +14358,6 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 - escalade@3.1.1: {} - escalade@3.2.0: {} escape-html@1.0.3: {} @@ -13626,9 +14366,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: @@ -13639,7 +14379,7 @@ snapshots: eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.32.0): dependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-node@0.3.10: dependencies: @@ -13649,10 +14389,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.7 is-bun-module: 2.0.0 @@ -13660,28 +14400,28 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - eslint: 9.39.4(jiti@2.6.1) + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-eslint-comments@3.2.0(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-eslint-comments@3.2.0(eslint@9.39.4(jiti@2.7.0)): dependencies: escape-string-regexp: 1.0.5 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) ignore: 5.2.4 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13690,9 +14430,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13704,13 +14444,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -13720,7 +14460,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -13729,23 +14469,23 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-promise@7.2.1(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-promise@7.3.0(eslint@9.39.4(jiti@2.7.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) - eslint: 9.39.4(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.0 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -13753,7 +14493,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.3.2 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.3 @@ -13767,15 +14507,15 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-regex@1.10.0(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-regex@1.10.0(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-storybook@10.3.5(eslint@9.39.4(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2): + eslint-plugin-storybook@10.4.2(eslint@9.39.4(jiti@2.7.0))(storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(typescript@6.0.2): dependencies: - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - eslint: 9.39.4(jiti@2.6.1) - storybook: 10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + eslint: 9.39.4(jiti@2.7.0) + storybook: 10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) transitivePeerDependencies: - supports-color - typescript @@ -13796,9 +14536,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4(jiti@2.6.1): + eslint@9.39.4(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 @@ -13833,7 +14573,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.3 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -13861,7 +14601,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.9 + '@types/estree': 1.0.8 esutils@2.0.3: {} @@ -13903,7 +14643,7 @@ snapshots: etag: 1.8.1 finalhandler: 1.3.2 fresh: 0.5.2 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 @@ -13916,7 +14656,7 @@ snapshots: send: 0.19.2 serve-static: 1.16.3 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -13949,8 +14689,18 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastest-levenshtein@1.0.12: {} fastq@1.15.0: @@ -13961,9 +14711,9 @@ snapshots: dependencies: websocket-driver: 0.7.4 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -14004,6 +14754,8 @@ snapshots: common-path-prefix: 3.0.0 pkg-dir: 7.0.0 + find-root@1.1.0: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -14039,7 +14791,7 @@ snapshots: dependencies: tslib: 2.8.1 - follow-redirects@1.15.11: {} + follow-redirects@1.16.0: {} for-each@0.3.3: dependencies: @@ -14054,7 +14806,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.2)(webpack@5.105.0): + fork-ts-checker-webpack-plugin@9.1.0(typescript@6.0.2)(webpack@5.107.2): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 @@ -14067,9 +14819,9 @@ snapshots: node-abort-controller: 3.1.1 schema-utils: 3.3.0 semver: 7.7.3 - tapable: 2.3.0 + tapable: 2.3.3 typescript: 6.0.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) form-data@4.0.4: dependencies: @@ -14087,20 +14839,23 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - formik@2.2.9(react@17.0.2): + formik@2.4.9(@types/react@17.0.75)(react@17.0.2): dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@17.0.75) deepmerge: 2.2.1 hoist-non-react-statics: 3.3.2 - lodash: 4.17.21 + lodash: 4.18.1 lodash-es: 4.17.21 react: 17.0.2 react-fast-compare: 2.0.4 tiny-warning: 1.0.3 - tslib: 1.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/react' forwarded@0.2.0: {} - fraction.js@4.3.6: {} + fraction.js@5.3.4: {} fresh@0.5.2: {} @@ -14258,7 +15013,7 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.12.0: {} + graphql@16.14.1: {} growly@1.3.0: {} @@ -14318,7 +15073,10 @@ snapshots: he@1.2.0: {} - headers-polyfill@4.0.2: {} + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.0 hermes-estree@0.25.1: {} @@ -14345,17 +15103,17 @@ snapshots: html-escaper@2.0.2: {} - html-loader@4.1.0(webpack@5.105.0): + html-loader@4.1.0(webpack@5.107.2): dependencies: html-minifier-terser: 6.1.0 parse5: 7.1.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) - html-loader@5.1.0(webpack@5.105.0): + html-loader@5.1.0(webpack@5.107.2): dependencies: html-minifier-terser: 7.2.0 parse5: 7.1.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) html-minifier-terser@6.1.0: dependencies: @@ -14383,14 +15141,14 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.5.3(webpack@5.105.0): + html-webpack-plugin@5.5.3(webpack@5.107.2): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 - lodash: 4.17.21 + lodash: 4.18.1 pretty-error: 4.0.0 tapable: 2.3.0 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) htmlparser2@6.1.0: dependencies: @@ -14415,14 +15173,6 @@ snapshots: setprototypeof: 1.1.0 statuses: 1.5.0 - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -14455,7 +15205,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -14464,6 +15214,13 @@ snapshots: http2-client@1.3.5: {} + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -14499,13 +15256,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.6): + icss-utils@5.1.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 - - icss-utils@5.1.0(postcss@8.5.9): - dependencies: - postcss: 8.5.9 + postcss: 8.5.15 ieee754@1.2.1: {} @@ -14833,7 +15586,7 @@ snapshots: jiti@1.18.2: {} - jiti@2.6.1: {} + jiti@2.7.0: {} jquery@3.6.0: {} @@ -14970,7 +15723,7 @@ snapshots: image-size: 0.5.5 make-dir: 2.1.0 mime: 1.6.0 - needle: 3.5.0 + needle: 3.3.1 source-map: 0.6.1 optional: true @@ -15035,6 +15788,8 @@ snapshots: lilconfig@2.1.0: {} + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} linkify-it@3.0.3: @@ -15066,7 +15821,7 @@ snapshots: rfdc: 1.3.0 wrap-ansi: 8.1.0 - loader-runner@4.3.1: {} + loader-runner@4.3.2: {} loader-utils@1.4.2: dependencies: @@ -15090,10 +15845,10 @@ snapshots: lodash-es@4.17.21: {} - lodash-webpack-plugin@0.11.6(webpack@5.105.0): + lodash-webpack-plugin@0.11.6(webpack@5.107.2): dependencies: - lodash: 4.17.21 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + lodash: 4.18.1 + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) lodash.debounce@4.0.8: {} @@ -15107,7 +15862,9 @@ snapshots: lodash.uniq@4.5.0: {} - lodash@4.17.21: {} + lodash@4.17.23: {} + + lodash@4.18.1: {} log-update@5.0.1: dependencies: @@ -15199,6 +15956,8 @@ snapshots: mdn-data@2.0.30: {} + mdn-data@2.27.1: {} + mdurl@1.0.1: {} media-typer@0.3.0: {} @@ -15224,7 +15983,7 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 - memoize-one@5.2.1: {} + memoize-one@6.0.0: {} merge-descriptors@1.0.3: {} @@ -15281,10 +16040,10 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.7.6(webpack@5.105.0): + mini-css-extract-plugin@2.7.6(webpack@5.107.2): dependencies: schema-utils: 4.0.0 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) minimalistic-assert@1.0.1: {} @@ -15302,11 +16061,11 @@ snapshots: minimist@1.2.8: {} - moment-timezone@0.5.41: + moment-timezone@0.6.2: dependencies: - moment: 2.29.4 + moment: 2.30.1 - moment@2.29.4: {} + moment@2.30.1: {} moo@0.5.2: {} @@ -15318,29 +16077,29 @@ snapshots: ms@2.1.3: {} - msw-storybook-addon@2.0.7(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2)): + msw-storybook-addon@2.0.7(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2)): dependencies: is-node-process: 1.2.0 - msw: 2.12.10(@types/node@25.0.3)(typescript@6.0.2) + msw: 2.14.6(@types/node@25.0.3)(typescript@6.0.2) - msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2): + msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2): dependencies: - '@inquirer/confirm': 5.1.21(@types/node@25.0.3) - '@mswjs/interceptors': 0.41.2 - '@open-draft/deferred-promise': 2.2.0 + '@inquirer/confirm': 6.1.1(@types/node@25.0.3) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 '@types/statuses': 2.0.6 cookie: 1.1.1 - graphql: 16.12.0 - headers-polyfill: 4.0.2 + graphql: 16.14.1 + headers-polyfill: 5.0.1 is-node-process: 1.2.0 outvariant: 1.4.3 path-to-regexp: 6.3.0 picocolors: 1.1.1 - rettime: 0.10.1 + rettime: 0.11.11 statuses: 2.0.2 strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.4.4 + tough-cookie: 6.0.1 + type-fest: 5.7.0 until-async: 3.0.2 yargs: 17.7.2 optionalDependencies: @@ -15355,7 +16114,7 @@ snapshots: mustache@4.2.0: {} - mute-stream@2.0.0: {} + mute-stream@3.0.0: {} mz@2.7.0: dependencies: @@ -15365,7 +16124,7 @@ snapshots: nanoclone@0.2.1: {} - nanoid@3.3.11: {} + nanoid@3.3.12: {} napi-postinstall@0.3.4: {} @@ -15378,7 +16137,7 @@ snapshots: railroad-diagrams: 1.0.0 randexp: 0.4.6 - needle@3.5.0: + needle@3.3.1: dependencies: iconv-lite: 0.6.3 sax: 1.6.0 @@ -15434,12 +16193,10 @@ snapshots: node-releases@2.0.14: {} - node-releases@2.0.27: {} + node-releases@2.0.46: {} normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - npm-run-path@5.1.0: dependencies: path-key: 4.0.0 @@ -15614,6 +16371,53 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.20.0: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.20.0 + '@oxc-resolver/binding-android-arm64': 11.20.0 + '@oxc-resolver/binding-darwin-arm64': 11.20.0 + '@oxc-resolver/binding-darwin-x64': 11.20.0 + '@oxc-resolver/binding-freebsd-x64': 11.20.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.20.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.20.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.20.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.20.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.20.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-x64-musl': 11.20.0 + '@oxc-resolver/binding-openharmony-arm64': 11.20.0 + '@oxc-resolver/binding-wasm32-wasi': 11.20.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.20.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.20.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -15714,8 +16518,6 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.3: {} - picomatch@4.0.4: {} pidtree@0.6.0: {} @@ -15740,7 +16542,7 @@ snapshots: dependencies: find-up: 6.3.0 - pkg-types@2.3.0: + pkg-types@2.3.1: dependencies: confbox: 0.2.4 exsolve: 1.0.8 @@ -15757,214 +16559,213 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@9.0.1(postcss@8.5.6): + postcss-calc@10.1.1(postcss@8.5.15): dependencies: - postcss: 8.5.6 - postcss-selector-parser: 6.0.13 + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-colormin@6.0.0(postcss@8.5.6): + postcss-colormin@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 + '@colordx/core': 5.4.3 + browserslist: 4.28.2 caniuse-api: 3.0.0 - colord: 2.9.2 - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-convert-values@6.0.0(postcss@8.5.6): + postcss-convert-values@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 - postcss: 8.5.6 + browserslist: 4.28.2 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-discard-comments@6.0.0(postcss@8.5.6): + postcss-discard-comments@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@6.0.0(postcss@8.5.6): + postcss-discard-duplicates@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 - postcss-discard-empty@6.0.0(postcss@8.5.6): + postcss-discard-empty@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 - postcss-discard-overridden@6.0.0(postcss@8.5.6): + postcss-discard-overridden@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 - postcss-import@15.1.0(postcss@8.5.6): + postcss-import@15.1.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.6 - postcss-js@4.0.1(postcss@8.5.6): + postcss-js@4.0.1(postcss@8.5.15): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.6 + postcss: 8.5.15 - postcss-load-config@4.0.1(postcss@8.5.6): + postcss-load-config@4.0.1(postcss@8.5.15): dependencies: lilconfig: 2.1.0 yaml: 2.7.0 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.15 - postcss-loader@7.3.3(postcss@8.5.6)(webpack@5.105.0): + postcss-loader@7.3.3(postcss@8.5.15)(webpack@5.107.2): dependencies: cosmiconfig: 8.2.0 jiti: 1.18.2 - postcss: 8.5.6 + postcss: 8.5.15 semver: 7.5.4 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) - postcss-merge-longhand@6.0.0(postcss@8.5.6): + postcss-merge-longhand@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - stylehacks: 6.0.0(postcss@8.5.6) + stylehacks: 8.0.0(postcss@8.5.15) - postcss-merge-rules@6.0.1(postcss@8.5.6): + postcss-merge-rules@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 caniuse-api: 3.0.0 - cssnano-utils: 4.0.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-selector-parser: 6.0.13 + cssnano-utils: 6.0.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 - postcss-minify-font-values@6.0.0(postcss@8.5.6): + postcss-minify-font-values@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.0(postcss@8.5.6): + postcss-minify-gradients@8.0.0(postcss@8.5.15): dependencies: - colord: 2.9.2 - cssnano-utils: 4.0.0(postcss@8.5.6) - postcss: 8.5.6 + '@colordx/core': 5.4.3 + cssnano-utils: 6.0.0(postcss@8.5.15) + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-minify-params@6.0.0(postcss@8.5.6): + postcss-minify-params@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 - cssnano-utils: 4.0.0(postcss@8.5.6) - postcss: 8.5.6 + browserslist: 4.28.2 + cssnano-utils: 6.0.0(postcss@8.5.15) + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.0(postcss@8.5.6): + postcss-minify-selectors@8.0.1(postcss@8.5.15): dependencies: - postcss: 8.5.6 - postcss-selector-parser: 6.0.13 + browserslist: 4.28.2 + caniuse-api: 3.0.0 + cssesc: 3.0.0 + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 - postcss-modules-extract-imports@3.0.0(postcss@8.5.6): + postcss-modules-extract-imports@3.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 - postcss-modules-extract-imports@3.1.0(postcss@8.5.9): + postcss-modules-extract-imports@3.1.0(postcss@8.5.15): dependencies: - postcss: 8.5.9 + postcss: 8.5.15 - postcss-modules-local-by-default@4.0.3(postcss@8.5.6): + postcss-modules-local-by-default@4.0.3(postcss@8.5.15): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 - postcss-modules-local-by-default@4.2.0(postcss@8.5.9): + postcss-modules-local-by-default@4.2.0(postcss@8.5.15): dependencies: - icss-utils: 5.1.0(postcss@8.5.9) - postcss: 8.5.9 + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.0.0(postcss@8.5.6): + postcss-modules-scope@3.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-selector-parser: 6.0.13 - postcss-modules-scope@3.2.1(postcss@8.5.9): + postcss-modules-scope@3.2.1(postcss@8.5.15): dependencies: - postcss: 8.5.9 + postcss: 8.5.15 postcss-selector-parser: 7.1.1 - postcss-modules-values@4.0.0(postcss@8.5.6): + postcss-modules-values@4.0.0(postcss@8.5.15): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.15) + postcss: 8.5.15 - postcss-modules-values@4.0.0(postcss@8.5.9): + postcss-nested@6.0.1(postcss@8.5.15): dependencies: - icss-utils: 5.1.0(postcss@8.5.9) - postcss: 8.5.9 - - postcss-nested@6.0.1(postcss@8.5.6): - dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-selector-parser: 6.0.13 - postcss-normalize-charset@6.0.0(postcss@8.5.6): + postcss-normalize-charset@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 - postcss-normalize-display-values@6.0.0(postcss@8.5.6): + postcss-normalize-display-values@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.0(postcss@8.5.6): + postcss-normalize-positions@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.0(postcss@8.5.6): + postcss-normalize-repeat-style@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.0(postcss@8.5.6): + postcss-normalize-string@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.0(postcss@8.5.6): + postcss-normalize-timing-functions@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.0.0(postcss@8.5.6): + postcss-normalize-unicode@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 - postcss: 8.5.6 + browserslist: 4.28.2 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.0(postcss@8.5.6): + postcss-normalize-url@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.0(postcss@8.5.6): + postcss-normalize-whitespace@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-ordered-values@6.0.0(postcss@8.5.6): + postcss-ordered-values@8.0.0(postcss@8.5.15): dependencies: - cssnano-utils: 4.0.0(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 6.0.0(postcss@8.5.15) + postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.0.0(postcss@8.5.6): + postcss-reduce-initial@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.15 - postcss-reduce-transforms@6.0.0(postcss@8.5.6): + postcss-reduce-transforms@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 postcss-selector-parser@6.0.13: @@ -15977,28 +16778,22 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@6.0.0(postcss@8.5.6): + postcss-svgo@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - svgo: 3.0.2 + svgo: 4.0.1 - postcss-unique-selectors@6.0.0(postcss@8.5.6): + postcss-unique-selectors@8.0.0(postcss@8.5.15): dependencies: - postcss: 8.5.6 - postcss-selector-parser: 6.0.13 + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.9: - dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -16014,7 +16809,7 @@ snapshots: pretty-error@4.0.0: dependencies: - lodash: 4.17.21 + lodash: 4.18.1 renderkid: 3.0.0 pretty-format@27.4.2: @@ -16041,7 +16836,7 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} prr@1.0.1: optional: true @@ -16132,14 +16927,14 @@ snapshots: optionalDependencies: '@types/react': 17.0.75 - react-codemirror-merge@4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.2)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + react-codemirror-merge@4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: '@babel/runtime': 7.26.0 '@codemirror/merge': 6.10.0 - '@codemirror/state': 6.5.1 - '@codemirror/theme-one-dark': 6.1.2 - '@codemirror/view': 6.36.2 - '@uiw/react-codemirror': 4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.2)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@codemirror/state': 6.6.0 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.0 + '@uiw/react-codemirror': 4.23.11(@babel/runtime@7.26.0)(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.43.0)(codemirror@6.0.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) codemirror: 6.0.1 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) @@ -16185,7 +16980,7 @@ snapshots: transitivePeerDependencies: - '@types/react-dom' - react-docgen-typescript-plugin@1.0.5(typescript@6.0.2)(webpack@5.105.0): + react-docgen-typescript-plugin@1.0.5(typescript@6.0.2)(webpack@5.107.2): dependencies: debug: 4.4.3 endent: 2.1.0 @@ -16195,7 +16990,7 @@ snapshots: react-docgen-typescript: 2.2.2(typescript@6.0.2) tslib: 2.8.1 typescript: 6.0.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) transitivePeerDependencies: - supports-color @@ -16209,7 +17004,7 @@ snapshots: '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.20.4 + '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 '@types/resolve': 1.20.6 doctrine: 3.0.0 @@ -16284,7 +17079,7 @@ snapshots: react-is@17.0.2: {} - react-json-view-lite@1.2.1(react@17.0.2): + react-json-view-lite@1.5.0(react@17.0.2): dependencies: react: 17.0.2 @@ -16307,33 +17102,35 @@ snapshots: optionalDependencies: '@types/react': 17.0.75 - react-select-async-paginate@0.7.11(@types/react@17.0.75)(react-select@5.2.1(@babel/core@7.23.6)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2): + react-select-async-paginate@0.7.11(@types/react@17.0.75)(react-select@5.10.2(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2): dependencies: '@seznam/compose-react-refs': 1.0.6 '@vtaits/use-lazy-ref': 0.1.4(react@17.0.2) krustykrab: 1.1.0 react: 17.0.2 - react-select: 5.2.1(@babel/core@7.23.6)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + react-select: 5.10.2(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) sleep-promise: 9.1.0 use-is-mounted-ref: 1.5.0(react@17.0.2) use-latest: 1.3.0(@types/react@17.0.75)(react@17.0.2) transitivePeerDependencies: - '@types/react' - react-select@5.2.1(@babel/core@7.23.6)(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + react-select@5.10.2(@types/react@17.0.75)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: '@babel/runtime': 7.26.0 '@emotion/cache': 11.6.0 - '@emotion/react': 11.6.0(@babel/core@7.23.6)(@types/react@17.0.75)(react@17.0.2) + '@emotion/react': 11.14.0(@types/react@17.0.75)(react@17.0.2) + '@floating-ui/dom': 1.7.6 '@types/react-transition-group': 4.4.4 - memoize-one: 5.2.1 + memoize-one: 6.0.0 prop-types: 15.8.1 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-transition-group: 4.4.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + use-isomorphic-layout-effect: 1.2.1(@types/react@17.0.75)(react@17.0.2) transitivePeerDependencies: - - '@babel/core' - '@types/react' + - supports-color react-style-singleton@2.2.3(@types/react@17.0.75)(react@17.0.2): dependencies: @@ -16494,7 +17291,7 @@ snapshots: css-select: 4.3.0 dom-converter: 0.2.0 htmlparser2: 6.1.0 - lodash: 4.17.21 + lodash: 4.18.1 strip-ansi: 6.0.1 require-directory@2.1.1: {} @@ -16547,7 +17344,7 @@ snapshots: retry@0.13.1: {} - rettime@0.10.1: {} + rettime@0.11.11: {} reusify@1.0.4: {} @@ -16561,26 +17358,26 @@ snapshots: dependencies: glob: 7.2.3 - rolldown@1.0.0-rc.15: + rolldown@1.0.3: dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 rollup@4.54.0: dependencies: @@ -16666,10 +17463,9 @@ snapshots: htmlparser2: 8.0.2 is-plain-object: 5.0.0 parse-srcset: 1.0.2 - postcss: 8.5.6 + postcss: 8.5.15 - sax@1.6.0: - optional: true + sax@1.6.0: {} saxes@6.0.0: dependencies: @@ -16742,10 +17538,6 @@ snapshots: dependencies: randombytes: 2.1.0 - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - serve-index@1.9.1: dependencies: accepts: 1.3.8 @@ -16769,6 +17561,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@3.1.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -16920,18 +17714,20 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@4.0.1(webpack@5.105.0): + source-map-loader@4.0.1(webpack@5.107.2): dependencies: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + source-map@0.5.7: {} + source-map@0.6.1: {} space-separated-tokens@2.0.2: {} @@ -16957,10 +17753,10 @@ snapshots: transitivePeerDependencies: - supports-color - speed-measure-webpack-plugin@1.5.0(webpack@5.105.0): + speed-measure-webpack-plugin@1.5.0(webpack@5.107.2): dependencies: chalk: 4.1.2 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) spinkit@2.0.1: {} @@ -16972,8 +17768,6 @@ snapshots: statuses@1.5.0: {} - statuses@2.0.1: {} - statuses@2.0.2: {} std-env@4.1.0: {} @@ -16989,10 +17783,10 @@ snapshots: storybook-css-modules-preset@1.1.1: {} - storybook@10.3.5(@testing-library/dom@9.3.4)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): + storybook@10.4.2(@testing-library/dom@9.3.4)(@types/react@17.0.75)(prettier@3.8.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2): dependencies: '@storybook/global': 5.0.0 - '@storybook/icons': 2.0.1(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@storybook/icons': 2.0.2(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4) '@vitest/expect': 3.2.4 @@ -17000,11 +17794,14 @@ snapshots: '@webcontainer/env': 1.1.1 esbuild: 0.27.3 open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.20.0 recast: 0.23.11 semver: 7.7.3 use-sync-external-store: 1.6.0(react@17.0.2) ws: 8.19.0 optionalDependencies: + '@types/react': 17.0.75 prettier: 3.8.3 transitivePeerDependencies: - '@testing-library/dom' @@ -17132,24 +17929,26 @@ snapshots: strip-json-comments@3.1.1: {} - style-loader@3.3.3(webpack@5.105.0): + style-loader@3.3.3(webpack@5.107.2): dependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) - style-loader@4.0.0(webpack@5.105.0): + style-loader@4.0.0(webpack@5.107.2): dependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) style-mod@4.1.2: {} - stylehacks@6.0.0(postcss@8.5.6): + stylehacks@8.0.0(postcss@8.5.15): dependencies: - browserslist: 4.28.1 - postcss: 8.5.6 - postcss-selector-parser: 6.0.13 + browserslist: 4.28.2 + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 stylis@4.0.10: {} + stylis@4.2.0: {} + sucrase@3.34.0: dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -17189,6 +17988,16 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + svgo@4.0.1: + dependencies: + commander: 11.1.0 + css-select: 5.1.0 + css-tree: 3.2.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + swagger2openapi@7.0.8: dependencies: call-me-maybe: 1.0.2 @@ -17205,11 +18014,11 @@ snapshots: transitivePeerDependencies: - encoding - swc-loader@0.2.7(@swc/core@1.15.11)(webpack@5.105.0): + swc-loader@0.2.7(@swc/core@1.15.11)(webpack@5.107.2): dependencies: '@swc/core': 1.15.11 '@swc/counter': 0.1.3 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) symbol-tree@3.2.4: {} @@ -17239,11 +18048,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.1(postcss@8.5.6) - postcss-nested: 6.0.1(postcss@8.5.6) + postcss: 8.5.15 + postcss-import: 15.1.0(postcss@8.5.15) + postcss-js: 4.0.1(postcss@8.5.15) + postcss-load-config: 4.0.1(postcss@8.5.15) + postcss-nested: 6.0.1(postcss@8.5.15) postcss-selector-parser: 6.0.13 resolve: 1.22.6 sucrase: 3.34.0 @@ -17252,22 +18061,25 @@ snapshots: tapable@2.3.0: {} - terser-webpack-plugin@5.3.16(@swc/core@1.15.11)(esbuild@0.27.3)(webpack@5.105.0): + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack@5.107.2): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) optionalDependencies: '@swc/core': 1.15.11 + cssnano: 8.0.1(postcss@8.5.15) esbuild: 0.27.3 + postcss: 8.5.15 terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -17301,8 +18113,13 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyrainbow@2.0.0: {} @@ -17343,7 +18160,7 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 - tough-cookie@6.0.0: + tough-cookie@6.0.1: dependencies: tldts: 7.0.23 @@ -17404,7 +18221,7 @@ snapshots: type-fest@1.4.0: {} - type-fest@5.4.4: + type-fest@5.7.0: dependencies: tagged-tag: 1.0.0 @@ -17473,13 +18290,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2): + typescript-eslint@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) - '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - eslint: 9.39.4(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.2) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.2) + eslint: 9.39.4(jiti@2.7.0) typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -17547,7 +18364,7 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.15.0 + acorn: 8.16.0 picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 @@ -17580,12 +18397,12 @@ snapshots: update-browserslist-db@1.0.13(browserslist@4.22.2): dependencies: browserslist: 4.22.2 - escalade: 3.1.1 + escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -17674,53 +18491,53 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-svgr@4.5.0(rollup@4.54.0)(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): + vite-plugin-svgr@4.5.0(rollup@4.54.0)(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.54.0) '@svgr/core': 8.1.0 '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) - vite: 8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) + vite: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) transitivePeerDependencies: - rollup - supports-color - vite-tsconfig-paths@4.3.1(typescript@6.0.2)(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): + vite-tsconfig-paths@4.3.1(typescript@6.0.2)(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.0.1(typescript@6.0.2) optionalDependencies: - vite: 8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) + vite: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) transitivePeerDependencies: - supports-color - typescript - vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2): + vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.15 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.0.3 esbuild: 0.27.3 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 less: 4.4.2 terser: 5.44.1 yaml: 1.10.2 - vitest@4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): + vitest@4.1.8(@types/node@25.0.3)(@vitest/coverage-v8@4.1.8)(jsdom@24.1.3)(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) + '@vitest/mocker': 4.1.8(msw@2.14.6(@types/node@25.0.3)(typescript@6.0.2))(vite@8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 '@vitest/spy': 4.1.8 '@vitest/utils': 4.1.8 - es-module-lexer: 2.1.0 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 @@ -17729,9 +18546,9 @@ snapshots: std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.0.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) + vite: 8.0.16(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.0.3 @@ -17765,11 +18582,11 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-build-notifier@3.1.0(webpack@5.105.0): + webpack-build-notifier@3.1.0(webpack@5.107.2): dependencies: node-notifier: 10.0.1 strip-ansi: 6.0.1 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) webpack-bundle-analyzer@5.2.0: dependencies: @@ -17788,12 +18605,12 @@ snapshots: - bufferutil - utf-8-validate - webpack-cli@6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0): + webpack-cli@6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.0) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.0) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.3)(webpack@5.105.0) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.107.2) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.107.2) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.4)(webpack@5.107.2) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 @@ -17802,13 +18619,13 @@ snapshots: import-local: 3.0.3 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) webpack-merge: 6.0.1 optionalDependencies: webpack-bundle-analyzer: 5.2.0 - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.105.0) + webpack-dev-server: 5.2.4(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.107.2) - webpack-dev-middleware@6.1.3(webpack@5.105.0): + webpack-dev-middleware@6.1.3(webpack@5.107.2): dependencies: colorette: 2.0.20 memfs: 3.6.0 @@ -17816,9 +18633,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.0): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.107.2): dependencies: colorette: 2.0.20 memfs: 4.56.10(tslib@2.8.1) @@ -17827,11 +18644,11 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) transitivePeerDependencies: - tslib - webpack-dev-server@5.2.3(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.105.0): + webpack-dev-server@5.2.4(tslib@2.8.1)(webpack-cli@6.0.1)(webpack@5.107.2): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -17859,11 +18676,11 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.0) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.107.2) ws: 8.19.0 optionalDependencies: - webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0) + webpack: 5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2) transitivePeerDependencies: - bufferutil - debug @@ -17883,44 +18700,49 @@ snapshots: flat: 5.0.2 wildcard: 2.0.1 - webpack-sources@3.3.3: {} - - webpack-virtual-modules@0.6.1: {} + webpack-sources@3.5.0: {} webpack-virtual-modules@0.6.2: {} - webpack@5.105.0(@swc/core@1.15.11)(esbuild@0.27.3)(webpack-cli@6.0.1): + webpack@5.107.2(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack-cli@6.0.1): dependencies: - '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.19.0 - es-module-lexer: 2.0.0 + enhanced-resolve: 5.22.1 + es-module-lexer: 2.1.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 + loader-runner: 4.3.2 + mime-db: 1.54.0 neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(@swc/core@1.15.11)(esbuild@0.27.3)(webpack@5.105.0) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.11)(cssnano@8.0.1(postcss@8.5.15))(esbuild@0.27.3)(postcss@8.5.15)(webpack@5.107.2) watchpack: 2.5.1 - webpack-sources: 3.3.3 + webpack-sources: 3.5.0 optionalDependencies: - webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.3)(webpack@5.105.0) + webpack-cli: 6.0.1(webpack-bundle-analyzer@5.2.0)(webpack-dev-server@5.2.4)(webpack@5.107.2) transitivePeerDependencies: + - '@minify-html/node' - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - uglify-js websocket-driver@0.7.4: @@ -18099,7 +18921,7 @@ snapshots: yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 @@ -18110,13 +18932,11 @@ snapshots: yocto-queue@1.0.0: {} - yoctocolors-cjs@2.1.3: {} - yup@0.32.11: dependencies: '@babel/runtime': 7.26.0 - '@types/lodash': 4.17.21 - lodash: 4.17.21 + '@types/lodash': 4.17.24 + lodash: 4.18.1 lodash-es: 4.17.21 nanoclone: 0.2.1 property-expr: 2.0.4