diff --git a/.env.example b/.env.example index 9e54a542..4d2795d6 100644 --- a/.env.example +++ b/.env.example @@ -113,6 +113,10 @@ PROXY_URL= EXTERNAL_SSL_CA_PATH= EXTERNAL_SSL_INSECURE= +## HTTP client timeout in seconds for external API calls (LLM providers, search tools, etc.) +## Default: 600 (10 minutes). Set to 0 to use the default. +HTTP_CLIENT_TIMEOUT= + ## Scraper URLs and settings ## For Docker (default): SCRAPER_PUBLIC_URL= diff --git a/README.md b/README.md index 75d575f3..ea259ae5 100644 --- a/README.md +++ b/README.md @@ -2870,6 +2870,7 @@ EMBEDDING_STRIP_NEW_LINES=true # Whether to remove new lines from text before e # Advanced settings PROXY_URL= # Optional proxy for all API calls +HTTP_CLIENT_TIMEOUT=600 # Timeout in seconds for external API calls (default: 600, 0 = no timeout) # SSL/TLS Certificate Configuration (for external communication with LLM backends and tool servers) EXTERNAL_SSL_CA_PATH= # Path to custom CA certificate file (PEM format) inside the container diff --git a/backend/cmd/installer/wizard/controller/controller.go b/backend/cmd/installer/wizard/controller/controller.go index 7a5e8923..6e0fe4cf 100644 --- a/backend/cmd/installer/wizard/controller/controller.go +++ b/backend/cmd/installer/wizard/controller/controller.go @@ -1912,6 +1912,7 @@ type ServerSettingsConfig struct { CorsOrigins loader.EnvVar // CORS_ORIGINS CookieSigningSalt loader.EnvVar // COOKIE_SIGNING_SALT ProxyURL loader.EnvVar // PROXY_URL + HTTPClientTimeout loader.EnvVar // HTTP_CLIENT_TIMEOUT ExternalSSLCAPath loader.EnvVar // EXTERNAL_SSL_CA_PATH ExternalSSLInsecure loader.EnvVar // EXTERNAL_SSL_INSECURE SSLDir loader.EnvVar // PENTAGI_SSL_DIR @@ -1932,6 +1933,7 @@ func (c *controller) GetServerSettingsConfig() *ServerSettingsConfig { "CORS_ORIGINS", "COOKIE_SIGNING_SALT", "PROXY_URL", + "HTTP_CLIENT_TIMEOUT", "EXTERNAL_SSL_CA_PATH", "EXTERNAL_SSL_INSECURE", "PENTAGI_SSL_DIR", @@ -1946,6 +1948,7 @@ func (c *controller) GetServerSettingsConfig() *ServerSettingsConfig { "CORS_ORIGINS": "https://localhost:8443", "PENTAGI_DATA_DIR": "pentagi-data", "PENTAGI_SSL_DIR": "pentagi-ssl", + "HTTP_CLIENT_TIMEOUT": "600", "EXTERNAL_SSL_INSECURE": "false", } @@ -1964,6 +1967,7 @@ func (c *controller) GetServerSettingsConfig() *ServerSettingsConfig { CorsOrigins: vars["CORS_ORIGINS"], CookieSigningSalt: vars["COOKIE_SIGNING_SALT"], ProxyURL: vars["PROXY_URL"], + HTTPClientTimeout: vars["HTTP_CLIENT_TIMEOUT"], ExternalSSLCAPath: vars["EXTERNAL_SSL_CA_PATH"], ExternalSSLInsecure: vars["EXTERNAL_SSL_INSECURE"], SSLDir: vars["PENTAGI_SSL_DIR"], @@ -2002,6 +2006,7 @@ func (c *controller) UpdateServerSettingsConfig(config *ServerSettingsConfig) er "CORS_ORIGINS": config.CorsOrigins.Value, "COOKIE_SIGNING_SALT": config.CookieSigningSalt.Value, "PROXY_URL": proxyURL, + "HTTP_CLIENT_TIMEOUT": config.HTTPClientTimeout.Value, "EXTERNAL_SSL_CA_PATH": config.ExternalSSLCAPath.Value, "EXTERNAL_SSL_INSECURE": config.ExternalSSLInsecure.Value, "PENTAGI_SSL_DIR": config.SSLDir.Value, @@ -2025,6 +2030,7 @@ func (c *controller) ResetServerSettingsConfig() *ServerSettingsConfig { "CORS_ORIGINS", "COOKIE_SIGNING_SALT", "PROXY_URL", + "HTTP_CLIENT_TIMEOUT", "EXTERNAL_SSL_CA_PATH", "EXTERNAL_SSL_INSECURE", "PENTAGI_SSL_DIR", diff --git a/backend/cmd/installer/wizard/locale/locale.go b/backend/cmd/installer/wizard/locale/locale.go index 48582e0c..ec71cbc6 100644 --- a/backend/cmd/installer/wizard/locale/locale.go +++ b/backend/cmd/installer/wizard/locale/locale.go @@ -1199,6 +1199,9 @@ const ( ServerSettingsProxyPassword = "Proxy Password" ServerSettingsProxyPasswordDesc = "Password for proxy authentication (optional)" + ServerSettingsHTTPClientTimeout = "HTTP Client Timeout" + ServerSettingsHTTPClientTimeoutDesc = "Timeout in seconds for external API calls (LLM providers, search engines, etc.)" + ServerSettingsExternalSSLCAPath = "Custom CA Certificate Path" ServerSettingsExternalSSLCAPathDesc = "Path inside container to custom root CA cert (e.g., /opt/pentagi/ssl/ca-bundle.pem)" @@ -1223,6 +1226,7 @@ const ( ServerSettingsProxyURLHint = "Proxy URL" ServerSettingsProxyUsernameHint = "Proxy Username" ServerSettingsProxyPasswordHint = "Proxy Password" + ServerSettingsHTTPClientTimeoutHint = "HTTP Timeout" ServerSettingsExternalSSLCAPathHint = "Custom CA Path" ServerSettingsExternalSSLInsecureHint = "Skip SSL Verification" ServerSettingsSSLDirHint = "SSL Directory" @@ -1256,6 +1260,16 @@ Examples: ServerSettingsProxyURLHelp = `HTTP or HTTPS proxy for outbound requests to LLM providers and external tools. Not used for Docker API communication.` + ServerSettingsHTTPClientTimeoutHelp = `Timeout in seconds for all external HTTP/HTTPS API calls including: +• LLM provider requests (OpenAI, Anthropic, Bedrock, etc.) +• Search engine queries (Google, Tavily, Perplexity, etc.) +• External tool integrations +• Embedding generation requests + +Default: 600 seconds (10 minutes) +Setting to 0 disables timeout (not recommended in production) +Too low values may cause legitimate long-running requests to fail.` + ServerSettingsExternalSSLCAPathHelp = `Path to custom CA certificate file (PEM format) inside the container. Must point to /opt/pentagi/ssl/ directory, which is mounted from pentagi-ssl volume on the host. @@ -2248,6 +2262,7 @@ const ( EnvDesc_CORS_ORIGINS = "PentAGI CORS Origins" EnvDesc_COOKIE_SIGNING_SALT = "PentAGI Cookie Signing Salt" EnvDesc_PROXY_URL = "HTTP/HTTPS Proxy URL" + EnvDesc_HTTP_CLIENT_TIMEOUT = "HTTP Client Timeout (seconds)" EnvDesc_EXTERNAL_SSL_CA_PATH = "Custom CA Certificate Path" EnvDesc_EXTERNAL_SSL_INSECURE = "Skip SSL Verification" EnvDesc_PENTAGI_SSL_DIR = "PentAGI SSL Directory" diff --git a/backend/cmd/installer/wizard/models/server_settings_form.go b/backend/cmd/installer/wizard/models/server_settings_form.go index f17df725..817e420f 100644 --- a/backend/cmd/installer/wizard/models/server_settings_form.go +++ b/backend/cmd/installer/wizard/models/server_settings_form.go @@ -94,6 +94,14 @@ func (m *ServerSettingsFormModel) BuildForm() tea.Cmd { true, )) + // http client timeout + fields = append(fields, m.createTextField("http_client_timeout", + locale.ServerSettingsHTTPClientTimeout, + locale.ServerSettingsHTTPClientTimeoutDesc, + config.HTTPClientTimeout, + false, + )) + // external ssl settings fields = append(fields, m.createTextField("external_ssl_ca_path", locale.ServerSettingsExternalSSLCAPath, @@ -265,6 +273,14 @@ func (m *ServerSettingsFormModel) GetCurrentConfiguration() string { sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsProxyPasswordHint, proxyPassword)) } + if httpTimeout := cfg.HTTPClientTimeout.Value; httpTimeout != "" { + httpTimeout = m.GetStyles().Info.Render(httpTimeout + "s") + sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsHTTPClientTimeoutHint, httpTimeout)) + } else if httpTimeout := cfg.HTTPClientTimeout.Default; httpTimeout != "" { + httpTimeout = m.GetStyles().Muted.Render(httpTimeout + "s") + sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsHTTPClientTimeoutHint, httpTimeout)) + } + if externalSSLCAPath := cfg.ExternalSSLCAPath.Value; externalSSLCAPath != "" { externalSSLCAPath = m.GetStyles().Info.Render(externalSSLCAPath) sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsExternalSSLCAPathHint, externalSSLCAPath)) @@ -334,6 +350,8 @@ func (m *ServerSettingsFormModel) GetHelpContent() string { sections = append(sections, locale.ServerSettingsCORSOriginsHelp) case "proxy_url": sections = append(sections, locale.ServerSettingsProxyURLHelp) + case "http_client_timeout": + sections = append(sections, locale.ServerSettingsHTTPClientTimeoutHelp) case "external_ssl_ca_path": sections = append(sections, locale.ServerSettingsExternalSSLCAPathHelp) case "external_ssl_insecure": @@ -363,6 +381,7 @@ func (m *ServerSettingsFormModel) HandleSave() error { CorsOrigins: cfg.CorsOrigins, CookieSigningSalt: cfg.CookieSigningSalt, ProxyURL: cfg.ProxyURL, + HTTPClientTimeout: cfg.HTTPClientTimeout, ExternalSSLCAPath: cfg.ExternalSSLCAPath, ExternalSSLInsecure: cfg.ExternalSSLInsecure, SSLDir: cfg.SSLDir, @@ -402,6 +421,15 @@ func (m *ServerSettingsFormModel) HandleSave() error { newCfg.ProxyUsername = value case "proxy_password": newCfg.ProxyPassword = value + case "http_client_timeout": + if value != "" { + if timeout, err := strconv.Atoi(value); err != nil { + return fmt.Errorf("invalid HTTP client timeout: must be a number") + } else if timeout < 0 { + return fmt.Errorf("invalid HTTP client timeout: must be >= 0") + } + } + newCfg.HTTPClientTimeout.Value = value case "external_ssl_ca_path": newCfg.ExternalSSLCAPath.Value = value case "external_ssl_insecure": diff --git a/backend/docs/config.md b/backend/docs/config.md index 702b2259..949884e2 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -1437,15 +1437,16 @@ These settings enable: Having multiple search engine options ensures redundancy and provides different search algorithms for varied information needs. Sploitus is specifically designed for security research, providing comprehensive exploit and vulnerability information essential for penetration testing. Searxng is particularly useful as it provides aggregated results from multiple search engines while offering enhanced privacy and customization options. -## Proxy Settings +## Network and Proxy Settings -These settings control the HTTP proxy used for outbound connections, which is important for network security and access control. +These settings control HTTP proxy, SSL configuration, and network timeouts for outbound connections, which are important for network security and access control. | Option | Environment Variable | Default Value | Description | | ------------------- | ----------------------- | ------------- | ---------------------------------------------------------------- | | ProxyURL | `PROXY_URL` | *(none)* | URL for HTTP proxy (e.g., `http://user:pass@proxy:8080`) | | ExternalSSLCAPath | `EXTERNAL_SSL_CA_PATH` | *(none)* | Path to trusted CA certificate for external LLM SSL connections | | ExternalSSLInsecure | `EXTERNAL_SSL_INSECURE` | `false` | Skip SSL certificate verification for external connections | +| HTTPClientTimeout | `HTTP_CLIENT_TIMEOUT` | `600` | Timeout in seconds for external API calls (0 = no timeout) | ### Usage Details @@ -1503,6 +1504,26 @@ The SSL settings provide additional security configuration: ``` **Warning**: Only use this in development or trusted environments. Skipping certificate verification exposes connections to man-in-the-middle attacks. +- **HTTPClientTimeout**: Sets the timeout for all external HTTP requests (LLM providers, search engines, etc.): + ```go + // Used in pkg/system/utils.go for HTTP client configuration + timeout := defaultHTTPClientTimeout + if cfg.HTTPClientTimeout > 0 { + timeout = time.Duration(cfg.HTTPClientTimeout) * time.Second + } + + httpClient := &http.Client{ + Timeout: timeout, + } + ``` + The default value of 600 seconds (10 minutes) is suitable for most LLM API calls, including long-running operations. Setting this to 0 disables the timeout (not recommended in production), while very low values may cause legitimate requests to fail. This setting affects: + - All LLM provider API calls (OpenAI, Anthropic, Bedrock, etc.) + - Search engine requests (Google, Tavily, Perplexity, etc.) + - External tool integrations + - Embedding generation requests + + Adjust this value based on your network conditions and the complexity of operations being performed. + ## Graphiti Knowledge Graph Settings These settings control the integration with Graphiti, a temporal knowledge graph system powered by Neo4j, for advanced semantic understanding and relationship tracking of AI agent operations. diff --git a/backend/go.mod b/backend/go.mod index 06310ac0..8d445cb3 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -54,22 +54,22 @@ require ( github.com/vxcontrol/langchaingo v0.1.14-update.5 github.com/wasilibs/go-re2 v1.10.0 github.com/xeipuuv/gojsonschema v1.2.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 - go.opentelemetry.io/otel/log v0.9.0 - go.opentelemetry.io/otel/metric v1.38.0 - go.opentelemetry.io/otel/sdk v1.36.0 - go.opentelemetry.io/otel/sdk/log v0.9.0 - go.opentelemetry.io/otel/sdk/metric v1.36.0 - go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/crypto v0.44.0 - golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.30.0 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 + go.opentelemetry.io/otel/log v0.14.0 + go.opentelemetry.io/otel/metric v1.39.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/otel/sdk/log v0.14.0 + go.opentelemetry.io/otel/sdk/metric v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 + golang.org/x/crypto v0.46.0 + golang.org/x/net v0.48.0 + golang.org/x/oauth2 v0.34.0 golang.org/x/sys v0.40.0 google.golang.org/api v0.238.0 - google.golang.org/grpc v1.73.0 + google.golang.org/grpc v1.79.3 gopkg.in/yaml.v3 v3.0.1 ) @@ -78,7 +78,7 @@ require ( cloud.google.com/go/aiplatform v1.85.0 // indirect cloud.google.com/go/auth v0.16.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/vertexai v0.12.0 // indirect @@ -108,7 +108,8 @@ require ( github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect @@ -127,7 +128,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gage-technologies/mistral-go v1.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -148,7 +149,7 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -214,22 +215,22 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/mod v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/genai v1.42.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.10 // indirect gotest.tools/v3 v3.5.1 // indirect nhooyr.io/websocket v1.8.7 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 30eb8bf9..a98135f0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -6,8 +6,8 @@ cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= @@ -121,6 +121,10 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -149,6 +153,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -197,6 +203,11 @@ github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= @@ -231,8 +242,8 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -331,8 +342,8 @@ github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8L github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -495,6 +506,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -668,32 +681,34 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 h1:gA2gh+3B3NDvRFP30Ufh7CC3TtJRbUSf2TTD0LbCagw= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0/go.mod h1:smRTR+02OtrVGjvWE1sQxhuazozKc/BXvvqqnmOxy+s= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= -go.opentelemetry.io/otel/log v0.9.0 h1:0OiWRefqJ2QszpCiqwGO0u9ajMPe17q6IscQvvp3czY= -go.opentelemetry.io/otel/log v0.9.0/go.mod h1:WPP4OJ+RBkQ416jrFCQFuFKtXKD6mOoYCQm6ykK8VaU= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/log v0.9.0 h1:YPCi6W1Eg0vwT/XJWsv2/PaQ2nyAJYuF7UUjQSBe3bc= -go.opentelemetry.io/otel/sdk/log v0.9.0/go.mod h1:y0HdrOz7OkXQBuc2yjiqnEHc+CRKeVhRE3hx4RwTmV4= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -713,8 +728,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -746,10 +761,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -760,8 +775,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -798,8 +813,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -810,8 +825,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= @@ -825,26 +840,28 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs= google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/genai v1.42.0 h1:XFHfo0DDCzdzQALZoFs6nowAHO2cE95XyVvFLNaFLRY= google.golang.org/genai v1.42.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= -google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= -google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index eae8a994..0d2789b0 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -193,6 +193,10 @@ type Config struct { ExternalSSLCAPath string `env:"EXTERNAL_SSL_CA_PATH" envDefault:""` ExternalSSLInsecure bool `env:"EXTERNAL_SSL_INSECURE" envDefault:"false"` + // HTTP client timeout in seconds for external API calls (LLM providers, search tools, etc.) + // A value of 0 means no timeout (not recommended). + HTTPClientTimeout int `env:"HTTP_CLIENT_TIMEOUT" envDefault:"600"` + // Telemetry (observability OpenTelemetry collector) TelemetryEndpoint string `env:"OTEL_HOST"` diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index 3af9e5df..10f7c787 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -1,8 +1,13 @@ package config import ( + "os" + "path/filepath" "testing" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/wasilibs/go-re2" "github.com/wasilibs/go-re2/experimental" ) @@ -44,7 +49,7 @@ func TestGetSecretPatterns_WithSecrets(t *testing.T) { func TestGetSecretPatterns_TrimsWhitespace(t *testing.T) { cfg := &Config{ - OpenAIKey: " sk-1234 ", + OpenAIKey: " sk-1234 ", GeminiAPIKey: "\tAIzaSyC123\n", } @@ -257,3 +262,325 @@ func TestGetSecretPatterns_AllFields(t *testing.T) { t.Logf("successfully compiled %d total regexes", len(regexes)) } + +// clearConfigEnv clears all environment variables referenced by Config struct tags +// so that tests are hermetic and not affected by ambient environment. +func clearConfigEnv(t *testing.T) { + t.Helper() + + envVars := []string{ + "DATABASE_URL", "DEBUG", "DATA_DIR", "ASK_USER", "INSTALLATION_ID", "LICENSE_KEY", + "DOCKER_INSIDE", "DOCKER_NET_ADMIN", "DOCKER_SOCKET", "DOCKER_NETWORK", + "DOCKER_PUBLIC_IP", "DOCKER_WORK_DIR", "DOCKER_DEFAULT_IMAGE", "DOCKER_DEFAULT_IMAGE_FOR_PENTEST", + "SERVER_PORT", "SERVER_HOST", "SERVER_USE_SSL", "SERVER_SSL_KEY", "SERVER_SSL_CRT", + "STATIC_URL", "STATIC_DIR", "CORS_ORIGINS", "COOKIE_SIGNING_SALT", + "SCRAPER_PUBLIC_URL", "SCRAPER_PRIVATE_URL", + "OPEN_AI_KEY", "OPEN_AI_SERVER_URL", + "ANTHROPIC_API_KEY", "ANTHROPIC_SERVER_URL", + "EMBEDDING_URL", "EMBEDDING_KEY", "EMBEDDING_MODEL", + "EMBEDDING_STRIP_NEW_LINES", "EMBEDDING_BATCH_SIZE", "EMBEDDING_PROVIDER", + "SUMMARIZER_PRESERVE_LAST", "SUMMARIZER_USE_QA", "SUMMARIZER_SUM_MSG_HUMAN_IN_QA", + "SUMMARIZER_LAST_SEC_BYTES", "SUMMARIZER_MAX_BP_BYTES", + "SUMMARIZER_MAX_QA_SECTIONS", "SUMMARIZER_MAX_QA_BYTES", "SUMMARIZER_KEEP_QA_SECTIONS", + "LLM_SERVER_URL", "LLM_SERVER_KEY", "LLM_SERVER_MODEL", "LLM_SERVER_PROVIDER", + "LLM_SERVER_CONFIG_PATH", "LLM_SERVER_LEGACY_REASONING", "LLM_SERVER_PRESERVE_REASONING", + "OLLAMA_SERVER_URL", "OLLAMA_SERVER_API_KEY", "OLLAMA_SERVER_MODEL", + "OLLAMA_SERVER_CONFIG_PATH", "OLLAMA_SERVER_PULL_MODELS_TIMEOUT", + "OLLAMA_SERVER_PULL_MODELS_ENABLED", "OLLAMA_SERVER_LOAD_MODELS_ENABLED", + "GEMINI_API_KEY", "GEMINI_SERVER_URL", + "BEDROCK_REGION", "BEDROCK_DEFAULT_AUTH", "BEDROCK_BEARER_TOKEN", + "BEDROCK_ACCESS_KEY_ID", "BEDROCK_SECRET_ACCESS_KEY", "BEDROCK_SESSION_TOKEN", "BEDROCK_SERVER_URL", + "DEEPSEEK_API_KEY", "DEEPSEEK_SERVER_URL", "DEEPSEEK_PROVIDER", + "GLM_API_KEY", "GLM_SERVER_URL", "GLM_PROVIDER", + "KIMI_API_KEY", "KIMI_SERVER_URL", "KIMI_PROVIDER", + "QWEN_API_KEY", "QWEN_SERVER_URL", "QWEN_PROVIDER", + "DUCKDUCKGO_ENABLED", "DUCKDUCKGO_REGION", "DUCKDUCKGO_SAFESEARCH", "DUCKDUCKGO_TIME_RANGE", + "SPLOITUS_ENABLED", + "GOOGLE_API_KEY", "GOOGLE_CX_KEY", "GOOGLE_LR_KEY", + "OAUTH_GOOGLE_CLIENT_ID", "OAUTH_GOOGLE_CLIENT_SECRET", + "OAUTH_GITHUB_CLIENT_ID", "OAUTH_GITHUB_CLIENT_SECRET", + "PUBLIC_URL", "TRAVERSAAL_API_KEY", "TAVILY_API_KEY", + "PERPLEXITY_API_KEY", "PERPLEXITY_MODEL", "PERPLEXITY_CONTEXT_SIZE", + "SEARXNG_URL", "SEARXNG_CATEGORIES", "SEARXNG_LANGUAGE", + "SEARXNG_SAFESEARCH", "SEARXNG_TIME_RANGE", "SEARXNG_TIMEOUT", + "ASSISTANT_USE_AGENTS", "ASSISTANT_SUMMARIZER_PRESERVE_LAST", + "ASSISTANT_SUMMARIZER_LAST_SEC_BYTES", "ASSISTANT_SUMMARIZER_MAX_BP_BYTES", + "ASSISTANT_SUMMARIZER_MAX_QA_SECTIONS", "ASSISTANT_SUMMARIZER_MAX_QA_BYTES", + "ASSISTANT_SUMMARIZER_KEEP_QA_SECTIONS", + "PROXY_URL", "EXTERNAL_SSL_CA_PATH", "EXTERNAL_SSL_INSECURE", "HTTP_CLIENT_TIMEOUT", + "OTEL_HOST", "LANGFUSE_BASE_URL", "LANGFUSE_PROJECT_ID", "LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY", + "GRAPHITI_ENABLED", "GRAPHITI_TIMEOUT", "GRAPHITI_URL", + "EXECUTION_MONITOR_ENABLED", "EXECUTION_MONITOR_SAME_TOOL_LIMIT", "EXECUTION_MONITOR_TOTAL_TOOL_LIMIT", + "MAX_GENERAL_AGENT_TOOL_CALLS", "MAX_LIMITED_AGENT_TOOL_CALLS", + "AGENT_PLANNING_STEP_ENABLED", + } + for _, v := range envVars { + t.Setenv(v, "") + } +} + +func TestNewConfig_Defaults(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + require.NotNil(t, config) + + assert.Equal(t, 8080, config.ServerPort) + assert.Equal(t, "0.0.0.0", config.ServerHost) + assert.Equal(t, false, config.Debug) + assert.Equal(t, "./data", config.DataDir) + assert.Equal(t, false, config.ServerUseSSL) + assert.Equal(t, "openai", config.EmbeddingProvider) + assert.Equal(t, 512, config.EmbeddingBatchSize) + assert.Equal(t, true, config.EmbeddingStripNewLines) + assert.Equal(t, true, config.DuckDuckGoEnabled) + assert.Equal(t, "debian:latest", config.DockerDefaultImage) + assert.Equal(t, "vxcontrol/kali-linux", config.DockerDefaultImageForPentest) +} + +func TestNewConfig_EnvOverride(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + t.Setenv("SERVER_PORT", "9090") + t.Setenv("SERVER_HOST", "127.0.0.1") + t.Setenv("DEBUG", "true") + + config, err := NewConfig() + require.NoError(t, err) + require.NotNil(t, config) + + assert.Equal(t, 9090, config.ServerPort) + assert.Equal(t, "127.0.0.1", config.ServerHost) + assert.Equal(t, true, config.Debug) +} + +func TestNewConfig_ProviderDefaults(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, "https://api.openai.com/v1", config.OpenAIServerURL) + assert.Equal(t, "https://api.anthropic.com/v1", config.AnthropicServerURL) + assert.Equal(t, "https://generativelanguage.googleapis.com", config.GeminiServerURL) + assert.Equal(t, "us-east-1", config.BedrockRegion) + assert.Equal(t, "https://api.deepseek.com", config.DeepSeekServerURL) + assert.Equal(t, "https://api.z.ai/api/paas/v4", config.GLMServerURL) + assert.Equal(t, "https://api.moonshot.ai/v1", config.KimiServerURL) + assert.Equal(t, "https://dashscope-us.aliyuncs.com/compatible-mode/v1", config.QwenServerURL) +} + +func TestNewConfig_StaticURL(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + t.Setenv("STATIC_URL", "https://example.com/static") + + config, err := NewConfig() + require.NoError(t, err) + require.NotNil(t, config.StaticURL) + + assert.Equal(t, "https", config.StaticURL.Scheme) + assert.Equal(t, "example.com", config.StaticURL.Host) + assert.Equal(t, "/static", config.StaticURL.Path) +} + +func TestNewConfig_StaticURL_Empty(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + assert.Nil(t, config.StaticURL) +} + +func TestNewConfig_SummarizerDefaults(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, true, config.SummarizerPreserveLast) + assert.Equal(t, true, config.SummarizerUseQA) + assert.Equal(t, false, config.SummarizerSumHumanInQA) + assert.Equal(t, 51200, config.SummarizerLastSecBytes) + assert.Equal(t, 16384, config.SummarizerMaxBPBytes) + assert.Equal(t, 10, config.SummarizerMaxQASections) + assert.Equal(t, 65536, config.SummarizerMaxQABytes) + assert.Equal(t, 1, config.SummarizerKeepQASections) +} + +func TestNewConfig_SearchEngineDefaults(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, "sonar", config.PerplexityModel) + assert.Equal(t, "low", config.PerplexityContextSize) + assert.Equal(t, "general", config.SearxngCategories) + assert.Equal(t, "0", config.SearxngSafeSearch) + assert.Equal(t, "lang_en", config.GoogleLRKey) +} + +func TestEnsureInstallationID_GeneratesNewUUID(t *testing.T) { + tmpDir := t.TempDir() + config := &Config{ + DataDir: tmpDir, + } + + ensureInstallationID(config) + + assert.NotEmpty(t, config.InstallationID) + assert.NoError(t, uuid.Validate(config.InstallationID)) + + // verify file was written + data, err := os.ReadFile(filepath.Join(tmpDir, "installation_id")) + require.NoError(t, err) + assert.Equal(t, config.InstallationID, string(data)) +} + +func TestEnsureInstallationID_ReadsExistingFile(t *testing.T) { + tmpDir := t.TempDir() + existingID := uuid.New().String() + err := os.WriteFile(filepath.Join(tmpDir, "installation_id"), []byte(existingID), 0644) + require.NoError(t, err) + + config := &Config{ + DataDir: tmpDir, + } + + ensureInstallationID(config) + + assert.Equal(t, existingID, config.InstallationID) +} + +func TestEnsureInstallationID_KeepsValidEnvValue(t *testing.T) { + envID := uuid.New().String() + config := &Config{ + InstallationID: envID, + DataDir: t.TempDir(), + } + + ensureInstallationID(config) + + assert.Equal(t, envID, config.InstallationID) +} + +func TestEnsureInstallationID_ReplacesInvalidEnvValue(t *testing.T) { + tmpDir := t.TempDir() + config := &Config{ + InstallationID: "not-a-valid-uuid", + DataDir: tmpDir, + } + + ensureInstallationID(config) + + assert.NotEqual(t, "not-a-valid-uuid", config.InstallationID) + assert.NoError(t, uuid.Validate(config.InstallationID)) +} + +func TestEnsureInstallationID_ReplacesInvalidFileContent(t *testing.T) { + tmpDir := t.TempDir() + err := os.WriteFile(filepath.Join(tmpDir, "installation_id"), []byte("garbage"), 0644) + require.NoError(t, err) + + config := &Config{ + DataDir: tmpDir, + } + + ensureInstallationID(config) + + assert.NotEqual(t, "garbage", config.InstallationID) + assert.NoError(t, uuid.Validate(config.InstallationID)) +} + +func TestNewConfig_CorsOrigins(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, []string{"*"}, config.CorsOrigins) +} + +func TestNewConfig_OllamaDefaults(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, 600, config.OllamaServerPullModelsTimeout) + assert.Equal(t, false, config.OllamaServerPullModelsEnabled) + assert.Equal(t, false, config.OllamaServerLoadModelsEnabled) +} + +func TestNewConfig_HTTPClientTimeout(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + t.Run("default timeout", func(t *testing.T) { + config, err := NewConfig() + require.NoError(t, err) + assert.Equal(t, 600, config.HTTPClientTimeout) + }) + + t.Run("custom timeout", func(t *testing.T) { + t.Setenv("HTTP_CLIENT_TIMEOUT", "300") + config, err := NewConfig() + require.NoError(t, err) + assert.Equal(t, 300, config.HTTPClientTimeout) + }) + + t.Run("zero timeout", func(t *testing.T) { + t.Setenv("HTTP_CLIENT_TIMEOUT", "0") + config, err := NewConfig() + require.NoError(t, err) + assert.Equal(t, 0, config.HTTPClientTimeout) + }) +} + +func TestNewConfig_AgentSupervisionDefaults(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, false, config.ExecutionMonitorEnabled) + assert.Equal(t, 5, config.ExecutionMonitorSameToolLimit) + assert.Equal(t, 10, config.ExecutionMonitorTotalToolLimit) + assert.Equal(t, 100, config.MaxGeneralAgentToolCalls) + assert.Equal(t, 20, config.MaxLimitedAgentToolCalls) + assert.Equal(t, false, config.AgentPlanningStepEnabled) +} + +func TestNewConfig_AgentSupervisionOverride(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + t.Setenv("EXECUTION_MONITOR_ENABLED", "true") + t.Setenv("EXECUTION_MONITOR_SAME_TOOL_LIMIT", "7") + t.Setenv("EXECUTION_MONITOR_TOTAL_TOOL_LIMIT", "15") + t.Setenv("MAX_GENERAL_AGENT_TOOL_CALLS", "150") + t.Setenv("MAX_LIMITED_AGENT_TOOL_CALLS", "30") + t.Setenv("AGENT_PLANNING_STEP_ENABLED", "true") + + config, err := NewConfig() + require.NoError(t, err) + + assert.Equal(t, true, config.ExecutionMonitorEnabled) + assert.Equal(t, 7, config.ExecutionMonitorSameToolLimit) + assert.Equal(t, 15, config.ExecutionMonitorTotalToolLimit) + assert.Equal(t, 150, config.MaxGeneralAgentToolCalls) + assert.Equal(t, 30, config.MaxLimitedAgentToolCalls) + assert.Equal(t, true, config.AgentPlanningStepEnabled) +} diff --git a/backend/pkg/graph/context_test.go b/backend/pkg/graph/context_test.go new file mode 100644 index 00000000..724e0f81 --- /dev/null +++ b/backend/pkg/graph/context_test.go @@ -0,0 +1,388 @@ +package graph + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- UserID --- + +func TestGetUserID_Found(t *testing.T) { + t.Parallel() + + ctx := SetUserID(t.Context(), 42) + id, err := GetUserID(ctx) + require.NoError(t, err) + assert.Equal(t, uint64(42), id) +} + +func TestGetUserID_Missing(t *testing.T) { + t.Parallel() + + _, err := GetUserID(t.Context()) + require.EqualError(t, err, "user ID not found") +} + +func TestGetUserID_WrongType(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(t.Context(), UserIDKey, "not-a-uint64") + _, err := GetUserID(ctx) + require.EqualError(t, err, "user ID not found") +} + +func TestSetUserID_Roundtrip(t *testing.T) { + t.Parallel() + + ctx := SetUserID(t.Context(), 99) + id, err := GetUserID(ctx) + require.NoError(t, err) + assert.Equal(t, uint64(99), id) +} + +// --- UserType --- + +func TestGetUserType_Found(t *testing.T) { + t.Parallel() + + ctx := SetUserType(t.Context(), "local") + ut, err := GetUserType(ctx) + require.NoError(t, err) + assert.Equal(t, "local", ut) +} + +func TestGetUserType_Missing(t *testing.T) { + t.Parallel() + + _, err := GetUserType(t.Context()) + require.EqualError(t, err, "user type not found") +} + +func TestGetUserType_WrongType(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(t.Context(), UserTypeKey, 123) + _, err := GetUserType(ctx) + require.EqualError(t, err, "user type not found") +} + +func TestSetUserType_Roundtrip(t *testing.T) { + t.Parallel() + + ctx := SetUserType(t.Context(), "oauth") + ut, err := GetUserType(ctx) + require.NoError(t, err) + assert.Equal(t, "oauth", ut) +} + +// --- UserPermissions --- + +func TestGetUserPermissions_Found(t *testing.T) { + t.Parallel() + + perms := []string{"flows.read", "flows.admin"} + ctx := SetUserPermissions(t.Context(), perms) + got, err := GetUserPermissions(ctx) + require.NoError(t, err) + assert.Equal(t, perms, got) +} + +func TestGetUserPermissions_Missing(t *testing.T) { + t.Parallel() + + _, err := GetUserPermissions(t.Context()) + require.EqualError(t, err, "user permissions not found") +} + +func TestGetUserPermissions_WrongType(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(t.Context(), UserPermissions, "not-a-slice") + _, err := GetUserPermissions(ctx) + require.EqualError(t, err, "user permissions not found") +} + +func TestSetUserPermissions_Roundtrip(t *testing.T) { + t.Parallel() + + perms := []string{"a.read", "b.write"} + ctx := SetUserPermissions(t.Context(), perms) + got, err := GetUserPermissions(ctx) + require.NoError(t, err) + assert.Equal(t, perms, got) +} + +// --- validateUserType --- + +func TestValidateUserType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ctx context.Context + allowed []string + wantOK bool + wantErr string + }{ + { + name: "allowed type", + ctx: SetUserType(t.Context(), "local"), + allowed: []string{"local", "oauth"}, + wantOK: true, + wantErr: "", + }, + { + name: "type missing from context", + ctx: t.Context(), + allowed: []string{"local"}, + wantOK: false, + wantErr: "unauthorized: invalid user type: user type not found", + }, + { + name: "unsupported type", + ctx: SetUserType(t.Context(), "apikey"), + allowed: []string{"local", "oauth"}, + wantOK: false, + wantErr: "unauthorized: invalid user type: apikey", + }, + { + name: "oauth type allowed", + ctx: SetUserType(t.Context(), "oauth"), + allowed: []string{"local", "oauth"}, + wantOK: true, + wantErr: "", + }, + { + name: "single allowed type matches", + ctx: SetUserType(t.Context(), "local"), + allowed: []string{"local"}, + wantOK: true, + wantErr: "", + }, + { + name: "empty allowed list", + ctx: SetUserType(t.Context(), "local"), + allowed: []string{}, + wantOK: false, + wantErr: "unauthorized: invalid user type: local", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ok, err := validateUserType(tc.ctx, tc.allowed...) + + assert.Equal(t, tc.wantOK, ok, "ok value mismatch") + + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +// --- validatePermission --- + +func TestValidatePermission(t *testing.T) { + t.Parallel() + + makeCtx := func(uid uint64, perms []string) context.Context { + ctx := SetUserID(t.Context(), uid) + return SetUserPermissions(ctx, perms) + } + + tests := []struct { + name string + ctx context.Context + perm string + wantUID int64 + wantAdmin bool + wantErr string + }{ + { + name: "exact permission match", + ctx: makeCtx(1, []string{"flows.read"}), + perm: "flows.read", + wantUID: 1, + wantAdmin: false, + wantErr: "", + }, + { + name: "admin permission via wildcard", + ctx: makeCtx(2, []string{"flows.admin"}), + perm: "flows.read", + wantUID: 2, + wantAdmin: true, + wantErr: "", + }, + { + name: "admin permission for write", + ctx: makeCtx(3, []string{"tasks.admin"}), + perm: "tasks.write", + wantUID: 3, + wantAdmin: true, + wantErr: "", + }, + { + name: "admin permission for delete", + ctx: makeCtx(4, []string{"users.admin"}), + perm: "users.delete", + wantUID: 4, + wantAdmin: true, + wantErr: "", + }, + { + name: "multiple permissions with admin", + ctx: makeCtx(5, []string{"flows.read", "tasks.admin", "users.write"}), + perm: "tasks.read", + wantUID: 5, + wantAdmin: true, + wantErr: "", + }, + { + name: "multiple permissions exact match", + ctx: makeCtx(6, []string{"flows.read", "tasks.write", "users.admin"}), + perm: "flows.read", + wantUID: 6, + wantAdmin: false, + wantErr: "", + }, + { + name: "user ID missing", + ctx: SetUserPermissions(t.Context(), []string{"flows.read"}), + perm: "flows.read", + wantErr: "unauthorized: invalid user: user ID not found", + }, + { + name: "permissions missing", + ctx: SetUserID(t.Context(), 3), + perm: "flows.read", + wantErr: "unauthorized: invalid user permissions: user permissions not found", + }, + { + name: "permission not found", + ctx: makeCtx(4, []string{"other.read"}), + perm: "flows.read", + wantErr: "requested permission 'flows.read' not found", + }, + { + name: "empty permissions list", + ctx: makeCtx(7, []string{}), + perm: "flows.read", + wantErr: "requested permission 'flows.read' not found", + }, + { + name: "permission without dot separator", + ctx: makeCtx(8, []string{"admin"}), + perm: "admin", + wantUID: 8, + wantAdmin: true, + wantErr: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + uid, admin, err := validatePermission(tc.ctx, tc.perm) + + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + assert.Equal(t, int64(0), uid, "uid should be 0 on error") + assert.False(t, admin, "admin should be false on error") + } else { + require.NoError(t, err) + assert.Equal(t, tc.wantUID, uid) + assert.Equal(t, tc.wantAdmin, admin) + } + }) + } +} + +func TestPermAdminRegexp(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + }{ + {"flows.read to flows.admin", "flows.read", "flows.admin"}, + {"tasks.write to tasks.admin", "tasks.write", "tasks.admin"}, + {"users.delete to users.admin", "users.delete", "users.admin"}, + {"assistants.create to assistants.admin", "assistants.create", "assistants.admin"}, + {"no dot separator", "admin", "admin"}, + {"multiple dots", "system.flows.read", "system.flows.admin"}, + {"uppercase action no match", "flows.READ", "flows.READ"}, + {"numbers in resource", "task123.read", "task123.admin"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := permAdminRegexp.ReplaceAllString(tt.input, "$1.admin") + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestValidatePermission_ZeroUserID(t *testing.T) { + t.Parallel() + + ctx := SetUserID(t.Context(), 0) + ctx = SetUserPermissions(ctx, []string{"flows.read"}) + + uid, admin, err := validatePermission(ctx, "flows.read") + require.NoError(t, err) + assert.Equal(t, int64(0), uid) + assert.False(t, admin) +} + +func TestValidatePermission_LargeUserID(t *testing.T) { + t.Parallel() + + ctx := SetUserID(t.Context(), 9223372036854775807) // max int64 + ctx = SetUserPermissions(ctx, []string{"flows.read"}) + + uid, admin, err := validatePermission(ctx, "flows.read") + require.NoError(t, err) + assert.Equal(t, int64(9223372036854775807), uid) + assert.False(t, admin) +} + +func TestGetUserID_ZeroValue(t *testing.T) { + t.Parallel() + + ctx := SetUserID(t.Context(), 0) + id, err := GetUserID(ctx) + require.NoError(t, err) + assert.Equal(t, uint64(0), id) +} + +func TestGetUserPermissions_EmptySlice(t *testing.T) { + t.Parallel() + + ctx := SetUserPermissions(t.Context(), []string{}) + perms, err := GetUserPermissions(ctx) + require.NoError(t, err) + assert.Equal(t, []string{}, perms) + assert.Len(t, perms, 0) +} + +func TestGetUserPermissions_NilSlice(t *testing.T) { + t.Parallel() + + ctx := SetUserPermissions(t.Context(), nil) + perms, err := GetUserPermissions(ctx) + require.NoError(t, err) + assert.Nil(t, perms) +} diff --git a/backend/pkg/providers/anthropic/anthropic.go b/backend/pkg/providers/anthropic/anthropic.go index 2bea0bc3..afeef483 100644 --- a/backend/pkg/providers/anthropic/anthropic.go +++ b/backend/pkg/providers/anthropic/anthropic.go @@ -20,6 +20,8 @@ var configFS embed.FS const AnthropicAgentModel = "claude-sonnet-4-20250514" +const AnthropicToolCallIDTemplate = "toolu_{r:24:b}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(AnthropicAgentModel), @@ -177,5 +179,5 @@ func (p *anthropicProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *anthropicProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, AnthropicToolCallIDTemplate) } diff --git a/backend/pkg/providers/bedrock/bedrock.go b/backend/pkg/providers/bedrock/bedrock.go index 722b0980..242451ee 100644 --- a/backend/pkg/providers/bedrock/bedrock.go +++ b/backend/pkg/providers/bedrock/bedrock.go @@ -30,6 +30,8 @@ var configFS embed.FS const BedrockAgentModel = bedrock.ModelAnthropicClaudeSonnet4 +const BedrockToolCallIDTemplate = "tooluse_{r:22:x}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(BedrockAgentModel), @@ -248,7 +250,7 @@ func (p *bedrockProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *bedrockProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, BedrockToolCallIDTemplate) } func extractToolsFromOptions(options []llms.CallOption) []llms.Tool { diff --git a/backend/pkg/providers/bedrock/bedrock_test.go b/backend/pkg/providers/bedrock/bedrock_test.go index 0f36b59a..71ec530e 100644 --- a/backend/pkg/providers/bedrock/bedrock_test.go +++ b/backend/pkg/providers/bedrock/bedrock_test.go @@ -731,6 +731,99 @@ func TestRestoreMissedToolsFromChain(t *testing.T) { } } }) + + t.Run("nil and empty tools both trigger restoration", func(t *testing.T) { + chain := []llms.MessageContent{ + { + Role: llms.ChatMessageTypeAI, + Parts: []llms.ContentPart{ + llms.ToolCall{ + ID: "c1", + Type: "function", + FunctionCall: &llms.FunctionCall{ + Name: "scan_tool", + Arguments: `{"target":"10.0.0.1"}`, + }, + }, + }, + }, + } + + // Both nil and empty slice should restore tools from chain + resultNil := restoreMissedToolsFromChain(chain, nil) + resultEmpty := restoreMissedToolsFromChain(chain, []llms.Tool{}) + + if len(resultNil) == 0 { + t.Error("expected tools restored from nil input") + } + if len(resultEmpty) == 0 { + t.Error("expected tools restored from empty slice input") + } + if len(resultNil) != len(resultEmpty) { + t.Errorf("nil and empty should produce same result: got %d vs %d", len(resultNil), len(resultEmpty)) + } + + // Verify the tool was properly inferred + if resultNil[0].Function == nil || resultNil[0].Function.Name != "scan_tool" { + t.Error("expected scan_tool to be restored") + } + }) + + t.Run("integration with extractToolsFromOptions", func(t *testing.T) { + chain := []llms.MessageContent{ + { + Role: llms.ChatMessageTypeAI, + Parts: []llms.ContentPart{ + llms.ToolCall{ + ID: "c1", + Type: "function", + FunctionCall: &llms.FunctionCall{ + Name: "nmap_scan", + Arguments: `{"port":"443"}`, + }, + }, + }, + }, + } + + // Simulate real usage: options with no tools, followed by restoration from chain + options := []llms.CallOption{ + llms.WithTemperature(0.7), + llms.WithMaxTokens(1000), + } + + extractedTools := extractToolsFromOptions(options) + if len(extractedTools) > 0 { + t.Error("expected no tools from options without WithTools") + } + + restored := restoreMissedToolsFromChain(chain, extractedTools) + if len(restored) == 0 { + t.Fatal("expected tools to be restored from chain when options contain no tools") + } + + found := false + for _, tool := range restored { + if tool.Function != nil && tool.Function.Name == "nmap_scan" { + found = true + schema, ok := tool.Function.Parameters.(map[string]any) + if !ok { + t.Fatal("expected inferred schema to be map[string]any") + } + props, ok := schema["properties"].(map[string]any) + if !ok { + t.Fatal("expected properties in inferred schema") + } + if _, exists := props["port"]; !exists { + t.Error("expected 'port' property in inferred schema") + } + break + } + } + if !found { + t.Error("expected nmap_scan tool to be restored") + } + }) } // TestExtractToolsFromOptions verifies tool extraction from CallOptions. diff --git a/backend/pkg/providers/custom/custom.go b/backend/pkg/providers/custom/custom.go index b86138ba..10eb914a 100644 --- a/backend/pkg/providers/custom/custom.go +++ b/backend/pkg/providers/custom/custom.go @@ -183,5 +183,5 @@ func (p *customProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *customProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, "") } diff --git a/backend/pkg/providers/deepseek/deepseek.go b/backend/pkg/providers/deepseek/deepseek.go index da2d7f0d..7494f2ae 100644 --- a/backend/pkg/providers/deepseek/deepseek.go +++ b/backend/pkg/providers/deepseek/deepseek.go @@ -21,6 +21,8 @@ var configFS embed.FS const DeepSeekAgentModel = "deepseek-chat" +const DeepSeekToolCallIDTemplate = "call_{r:2:d}_{r:24:b}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(DeepSeekAgentModel), @@ -175,5 +177,5 @@ func (p *deepseekProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *deepseekProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, DeepSeekToolCallIDTemplate) } diff --git a/backend/pkg/providers/embeddings/embedder_test.go b/backend/pkg/providers/embeddings/embedder_test.go new file mode 100644 index 00000000..45c360c5 --- /dev/null +++ b/backend/pkg/providers/embeddings/embedder_test.go @@ -0,0 +1,412 @@ +package embeddings + +import ( + "testing" + + "pentagi/pkg/config" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew_AllProviders(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + provider string + available bool + }{ + {"openai", "openai", true}, + {"ollama", "ollama", true}, + {"mistral", "mistral", true}, + {"jina", "jina", true}, + {"huggingface", "huggingface", true}, + {"googleai", "googleai", true}, + {"voyageai", "voyageai", true}, + {"none", "none", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: tt.provider, + EmbeddingKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.Equal(t, tt.available, e.IsAvailable()) + }) + } +} + +func TestNew_UnsupportedProvider(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "unknown-provider", + } + + e, err := New(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported embedding provider") + require.NotNil(t, e) + assert.False(t, e.IsAvailable()) +} + +func TestNew_OpenAI_DefaultModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_OpenAI_CustomURL(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + EmbeddingURL: "https://custom-openai.example.com", + EmbeddingKey: "custom-key", + EmbeddingModel: "text-embedding-3-small", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_OpenAI_FallbackToOpenAIServerURL(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + OpenAIServerURL: "https://api.openai.com/v1", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_OpenAI_KeyPriority(t *testing.T) { + t.Parallel() + + t.Run("EmbeddingKey takes priority", func(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + EmbeddingKey: "embedding-specific-key", + OpenAIKey: "generic-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) + }) + + t.Run("Falls back to OpenAIKey", func(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "generic-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) + }) +} + +func TestNew_Jina_DefaultModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "jina", + EmbeddingKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_Huggingface_DefaultModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "huggingface", + EmbeddingKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_GoogleAI_DefaultModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "googleai", + EmbeddingKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_VoyageAI_DefaultModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "voyageai", + EmbeddingKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_WithBatchSizeAndStripNewLines(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + EmbeddingBatchSize: 100, + EmbeddingStripNewLines: true, + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_HTTPClientError(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + ExternalSSLCAPath: "/non/existent/ca.pem", + EmbeddingBatchSize: 512, + } + + _, err := New(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to read external CA certificate") +} + +func TestIsAvailable_NilEmbedder(t *testing.T) { + t.Parallel() + + e := &embedder{nil} + assert.False(t, e.IsAvailable()) +} + +func TestIsAvailable_ValidEmbedder(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_Ollama_WithCustomModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "ollama", + EmbeddingURL: "http://localhost:11434", + EmbeddingModel: "nomic-embed-text", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_Mistral_WithCustomURL(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "mistral", + EmbeddingKey: "test-key", + EmbeddingURL: "https://api.mistral.ai", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_Jina_WithCustomModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "jina", + EmbeddingKey: "test-key", + EmbeddingModel: "jina-embeddings-v2-base-en", + EmbeddingURL: "https://api.jina.ai/v1", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_Huggingface_WithCustomModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "huggingface", + EmbeddingKey: "test-key", + EmbeddingModel: "sentence-transformers/all-MiniLM-L6-v2", + EmbeddingURL: "https://api-inference.huggingface.co", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_GoogleAI_WithCustomModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "googleai", + EmbeddingKey: "test-key", + EmbeddingModel: "text-embedding-004", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_VoyageAI_WithCustomModel(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "voyageai", + EmbeddingKey: "test-key", + EmbeddingModel: "voyage-code-3", + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) +} + +func TestNew_DifferentBatchSizes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + batchSize int + }{ + {"default", 0}, + {"small batch", 10}, + {"medium batch", 100}, + {"large batch", 512}, + {"very large batch", 2048}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + EmbeddingBatchSize: tt.batchSize, + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) + }) + } +} + +func TestNew_StripNewLinesVariations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + stripNewLines bool + }{ + {"strip enabled", true}, + {"strip disabled", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "openai", + OpenAIKey: "test-key", + EmbeddingStripNewLines: tt.stripNewLines, + } + + e, err := New(cfg) + require.NoError(t, err) + require.NotNil(t, e) + assert.True(t, e.IsAvailable()) + }) + } +} + +func TestNew_EmptyProvider(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + EmbeddingProvider: "", + } + + e, err := New(cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported embedding provider") + require.NotNil(t, e) + assert.False(t, e.IsAvailable()) +} diff --git a/backend/pkg/providers/gemini/gemini.go b/backend/pkg/providers/gemini/gemini.go index b2787527..dd6b0e6d 100644 --- a/backend/pkg/providers/gemini/gemini.go +++ b/backend/pkg/providers/gemini/gemini.go @@ -25,6 +25,8 @@ const GeminiAgentModel = "gemini-2.5-flash" const defaultGeminiHost = "generativelanguage.googleapis.com" +const GeminiToolCallIDTemplate = "{r:8:x}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(GeminiAgentModel), @@ -186,5 +188,5 @@ func (p *geminiProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *geminiProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, GeminiToolCallIDTemplate) } diff --git a/backend/pkg/providers/glm/glm.go b/backend/pkg/providers/glm/glm.go index a61540b9..55e083bf 100644 --- a/backend/pkg/providers/glm/glm.go +++ b/backend/pkg/providers/glm/glm.go @@ -21,6 +21,8 @@ var configFS embed.FS const GLMAgentModel = "glm-4.7-flashx" +const GLMToolCallIDTemplate = "call_-{r:19:d}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(GLMAgentModel), @@ -173,5 +175,5 @@ func (p *glmProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *glmProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, GLMToolCallIDTemplate) } diff --git a/backend/pkg/providers/kimi/config.yml b/backend/pkg/providers/kimi/config.yml index 716a4013..e07764f1 100644 --- a/backend/pkg/providers/kimi/config.yml +++ b/backend/pkg/providers/kimi/config.yml @@ -1,15 +1,17 @@ simple: model: "kimi-k2-turbo-preview" - temperature: 0.6 + temperature: 0.3 n: 1 max_tokens: 8192 + extra_body: + tool_choice: "auto" price: input: 1.15 output: 8.0 simple_json: model: "kimi-k2-turbo-preview" - temperature: 0.6 + temperature: 0.3 n: 1 max_tokens: 4096 json: true @@ -22,6 +24,8 @@ primary_agent: temperature: 1.0 n: 1 max_tokens: 16384 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 3.0 @@ -31,6 +35,8 @@ assistant: temperature: 1.0 n: 1 max_tokens: 16384 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 3.0 @@ -40,6 +46,8 @@ generator: temperature: 1.0 n: 1 max_tokens: 32768 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 3.0 @@ -49,6 +57,8 @@ refiner: temperature: 1.0 n: 1 max_tokens: 20480 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 3.0 @@ -67,6 +77,8 @@ reflector: temperature: 0.7 n: 1 max_tokens: 4096 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 2.5 @@ -76,6 +88,8 @@ searcher: temperature: 0.7 n: 1 max_tokens: 4096 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 2.5 @@ -85,6 +99,8 @@ enricher: temperature: 0.7 n: 1 max_tokens: 4096 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 2.5 @@ -94,6 +110,8 @@ coder: temperature: 1.0 n: 1 max_tokens: 20480 + extra_body: + tool_choice: "auto" price: input: 0.6 output: 3.0 @@ -103,6 +121,8 @@ installer: temperature: 0.7 n: 1 max_tokens: 16384 + extra_body: + tool_choice: "auto" price: input: 1.15 output: 8.0 @@ -112,6 +132,8 @@ pentester: temperature: 0.8 n: 1 max_tokens: 16384 + extra_body: + tool_choice: "auto" price: input: 1.15 output: 8.0 diff --git a/backend/pkg/providers/kimi/kimi.go b/backend/pkg/providers/kimi/kimi.go index aa0aa9e9..c7c3b20b 100644 --- a/backend/pkg/providers/kimi/kimi.go +++ b/backend/pkg/providers/kimi/kimi.go @@ -21,6 +21,8 @@ var configFS embed.FS const KimiAgentModel = "kimi-k2-turbo-preview" +const KimiToolCallIDTemplate = "{f}:{r:1:d}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(KimiAgentModel), @@ -175,5 +177,5 @@ func (p *kimiProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *kimiProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, KimiToolCallIDTemplate) } diff --git a/backend/pkg/providers/ollama/ollama.go b/backend/pkg/providers/ollama/ollama.go index 3a5dd9b8..4bcdcd64 100644 --- a/backend/pkg/providers/ollama/ollama.go +++ b/backend/pkg/providers/ollama/ollama.go @@ -295,5 +295,5 @@ func (p *ollamaProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *ollamaProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, "") } diff --git a/backend/pkg/providers/openai/openai.go b/backend/pkg/providers/openai/openai.go index 102c79d9..0c8cb22c 100644 --- a/backend/pkg/providers/openai/openai.go +++ b/backend/pkg/providers/openai/openai.go @@ -20,6 +20,8 @@ var configFS embed.FS const OpenAIAgentModel = "o4-mini" +const OpenAIToolCallIDTemplate = "call_{r:24:b}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(OpenAIAgentModel), @@ -169,5 +171,5 @@ func (p *openaiProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *openaiProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, OpenAIToolCallIDTemplate) } diff --git a/backend/pkg/providers/provider/agents.go b/backend/pkg/providers/provider/agents.go index f5d48762..ba21fb40 100644 --- a/backend/pkg/providers/provider/agents.go +++ b/backend/pkg/providers/provider/agents.go @@ -46,6 +46,44 @@ func storeInCache(provider Provider, template string) { cacheTemplates.Store(provider.Type(), template) } +// testTemplate validates a template by collecting a single sample from the LLM +// and checking if it matches the provided template pattern. +// Returns true if the template is valid, false otherwise (including any errors). +// This function makes a real LLM call to collect samples for validation. +func testTemplate( + ctx context.Context, + provider Provider, + opt pconfig.ProviderOptionsType, + prompter templates.Prompter, + template string, +) bool { + // If no template provided, skip validation + if template == "" { + return false + } + + // Collect one sample to validate the template + samples, err := runToolCallIDCollector(ctx, provider, opt, prompter) + if err != nil { + // Any error means validation failed + return false + } + + // If no samples collected, validation failed + if len(samples) == 0 { + return false + } + + // Validate the template against collected samples + if err := templates.ValidatePattern(template, samples); err != nil { + // Template doesn't match + return false + } + + // Template validated successfully + return true +} + // DetermineToolCallIDTemplate analyzes tool call ID format by collecting samples // and using AI to detect the pattern, with fallback to heuristic analysis func DetermineToolCallIDTemplate( @@ -53,6 +91,7 @@ func DetermineToolCallIDTemplate( provider Provider, opt pconfig.ProviderOptionsType, prompter templates.Prompter, + defaultTemplate string, ) (string, error) { ctx, observation := obs.Observer.NewObservation(ctx) agent := observation.Agent( @@ -84,6 +123,12 @@ func DetermineToolCallIDTemplate( return wrapEndAgentSpan(template, "found in cache", nil) } + // Step 0.5: Test default template if provided (makes one LLM call for validation) + if defaultTemplate != "" && testTemplate(ctx, provider, opt, prompter, defaultTemplate) { + storeInCache(provider, defaultTemplate) + return wrapEndAgentSpan(defaultTemplate, "validated default template", nil) + } + // Step 1: Collect 5 sample tool call IDs in parallel samples, err := collectToolCallIDSamples(ctx, provider, opt, prompter) if err != nil { @@ -244,9 +289,15 @@ func runToolCallIDCollector( // Call LLM with tool messages := []llms.MessageContent{ { - Role: llms.ChatMessageTypeHuman, + Role: llms.ChatMessageTypeSystem, Parts: []llms.ContentPart{llms.TextContent{Text: prompt}}, }, + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{llms.TextContent{ + Text: fmt.Sprintf("Call the %s function", testFunctionName), + }}, + }, } response, err := provider.CallWithTools(ctx, opt, messages, []llms.Tool{testTool}, nil) @@ -332,9 +383,15 @@ func detectPatternWithAI( // Call LLM with tool messages := []llms.MessageContent{ { - Role: llms.ChatMessageTypeHuman, + Role: llms.ChatMessageTypeSystem, Parts: []llms.ContentPart{llms.TextContent{Text: prompt}}, }, + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{llms.TextContent{ + Text: fmt.Sprintf("Submit the detected pattern template for the function %s", patternFunctionName), + }}, + }, } response, err := provider.CallWithTools(ctx, opt, messages, []llms.Tool{patternTool}, nil) diff --git a/backend/pkg/providers/provider/agents_test.go b/backend/pkg/providers/provider/agents_test.go index c237db43..35a50be0 100644 --- a/backend/pkg/providers/provider/agents_test.go +++ b/backend/pkg/providers/provider/agents_test.go @@ -150,6 +150,11 @@ func TestDetermineMinimalCharset(t *testing.T) { chars: []byte{'0', '5', 'a', 'z'}, expected: "x", // digit + lower but no upper = alnum }, + { + name: "digit_upper_only", + chars: []byte{'0', '5', 'A', 'Z'}, + expected: "x", // digit + upper but no lower = alnum + }, } for _, tc := range testCases { @@ -161,3 +166,56 @@ func TestDetermineMinimalCharset(t *testing.T) { }) } } + +func TestDetermineCommonCharset(t *testing.T) { + testCases := []struct { + name string + chars [][]byte + expected string + }{ + { + name: "all digits across positions", + chars: [][]byte{ + {'1', '2', '3'}, + {'4', '5', '6'}, + {'7', '8', '9'}, + }, + expected: "d", + }, + { + name: "hex lowercase across positions", + chars: [][]byte{ + {'a', 'b', 'c'}, + {'d', 'e', 'f'}, + {'0', '1', '2'}, + }, + expected: "h", + }, + { + name: "base62 across positions", + chars: [][]byte{ + {'a', 'B', 'c'}, + {'D', 'e', 'F'}, + {'0', '1', '2'}, + }, + expected: "b", + }, + { + name: "only lowercase across positions", + chars: [][]byte{ + {'a', 'b', 'c'}, + {'x', 'y', 'z'}, + }, + expected: "h", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := determineCommonCharset(tc.chars) + if result != tc.expected { + t.Errorf("Expected charset '%s', got '%s'", tc.expected, result) + } + }) + } +} diff --git a/backend/pkg/providers/qwen/qwen.go b/backend/pkg/providers/qwen/qwen.go index 929a29c7..84b46830 100644 --- a/backend/pkg/providers/qwen/qwen.go +++ b/backend/pkg/providers/qwen/qwen.go @@ -21,6 +21,8 @@ var configFS embed.FS const QwenAgentModel = "qwen-plus" +const QwenToolCallIDTemplate = "call_{r:24:h}" + func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { defaultOptions := []llms.CallOption{ llms.WithModel(QwenAgentModel), @@ -174,5 +176,5 @@ func (p *qwenProvider) GetUsage(info map[string]any) pconfig.CallUsage { } func (p *qwenProvider) GetToolCallIDTemplate(ctx context.Context, prompter templates.Prompter) (string, error) { - return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter) + return provider.DetermineToolCallIDTemplate(ctx, p, pconfig.OptionsTypeSimple, prompter, QwenToolCallIDTemplate) } diff --git a/backend/pkg/server/context/context_test.go b/backend/pkg/server/context/context_test.go new file mode 100644 index 00000000..ebb3f24c --- /dev/null +++ b/backend/pkg/server/context/context_test.go @@ -0,0 +1,510 @@ +package context + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestGetInt64(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(c *gin.Context) + key string + wantVal int64 + wantOK bool + }{ + { + name: "found", + setup: func(c *gin.Context) { c.Set("id", int64(42)) }, + key: "id", + wantVal: 42, + wantOK: true, + }, + { + name: "missing", + setup: func(c *gin.Context) {}, + key: "id", + wantVal: 0, + wantOK: false, + }, + { + name: "wrong type string", + setup: func(c *gin.Context) { c.Set("id", "not-an-int") }, + key: "id", + wantVal: 0, + wantOK: false, + }, + { + name: "wrong type uint64", + setup: func(c *gin.Context) { c.Set("id", uint64(99)) }, + key: "id", + wantVal: 0, + wantOK: false, + }, + { + name: "zero value", + setup: func(c *gin.Context) { c.Set("id", int64(0)) }, + key: "id", + wantVal: 0, + wantOK: true, + }, + { + name: "negative value", + setup: func(c *gin.Context) { c.Set("id", int64(-100)) }, + key: "id", + wantVal: -100, + wantOK: true, + }, + { + name: "max int64", + setup: func(c *gin.Context) { c.Set("id", int64(9223372036854775807)) }, + key: "id", + wantVal: 9223372036854775807, + wantOK: true, + }, + { + name: "different key", + setup: func(c *gin.Context) { c.Set("other", int64(123)) }, + key: "id", + wantVal: 0, + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + tt.setup(c) + val, ok := GetInt64(c, tt.key) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.wantVal, val) + }) + } +} + +func TestGetUint64(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(c *gin.Context) + key string + wantVal uint64 + wantOK bool + }{ + { + name: "found", + setup: func(c *gin.Context) { c.Set("uid", uint64(99)) }, + key: "uid", + wantVal: 99, + wantOK: true, + }, + { + name: "missing", + setup: func(c *gin.Context) {}, + key: "uid", + wantVal: 0, + wantOK: false, + }, + { + name: "wrong type int64", + setup: func(c *gin.Context) { c.Set("uid", int64(99)) }, + key: "uid", + wantVal: 0, + wantOK: false, + }, + { + name: "wrong type string", + setup: func(c *gin.Context) { c.Set("uid", "99") }, + key: "uid", + wantVal: 0, + wantOK: false, + }, + { + name: "zero value", + setup: func(c *gin.Context) { c.Set("uid", uint64(0)) }, + key: "uid", + wantVal: 0, + wantOK: true, + }, + { + name: "large value", + setup: func(c *gin.Context) { c.Set("uid", uint64(18446744073709551615)) }, + key: "uid", + wantVal: 18446744073709551615, + wantOK: true, + }, + { + name: "different key", + setup: func(c *gin.Context) { c.Set("other", uint64(456)) }, + key: "uid", + wantVal: 0, + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + tt.setup(c) + val, ok := GetUint64(c, tt.key) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.wantVal, val) + }) + } +} + +func TestGetString(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(c *gin.Context) + key string + wantVal string + wantOK bool + }{ + { + name: "found", + setup: func(c *gin.Context) { c.Set("name", "alice") }, + key: "name", + wantVal: "alice", + wantOK: true, + }, + { + name: "missing", + setup: func(c *gin.Context) {}, + key: "name", + wantVal: "", + wantOK: false, + }, + { + name: "wrong type int", + setup: func(c *gin.Context) { c.Set("name", 123) }, + key: "name", + wantVal: "", + wantOK: false, + }, + { + name: "wrong type bool", + setup: func(c *gin.Context) { c.Set("name", true) }, + key: "name", + wantVal: "", + wantOK: false, + }, + { + name: "empty string", + setup: func(c *gin.Context) { c.Set("name", "") }, + key: "name", + wantVal: "", + wantOK: true, + }, + { + name: "long string", + setup: func(c *gin.Context) { c.Set("name", "very-long-string-with-special-chars-@#$%") }, + key: "name", + wantVal: "very-long-string-with-special-chars-@#$%", + wantOK: true, + }, + { + name: "different key", + setup: func(c *gin.Context) { c.Set("other", "value") }, + key: "name", + wantVal: "", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + tt.setup(c) + val, ok := GetString(c, tt.key) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.wantVal, val) + }) + } +} + +func TestGetStringArray(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(c *gin.Context) + key string + wantVal []string + wantOK bool + }{ + { + name: "found", + setup: func(c *gin.Context) { c.Set("perms", []string{"read", "write"}) }, + key: "perms", + wantVal: []string{"read", "write"}, + wantOK: true, + }, + { + name: "missing", + setup: func(c *gin.Context) {}, + key: "perms", + wantVal: []string{}, + wantOK: false, + }, + { + name: "wrong type string", + setup: func(c *gin.Context) { c.Set("perms", "not-a-slice") }, + key: "perms", + wantVal: []string{}, + wantOK: false, + }, + { + name: "wrong type int slice", + setup: func(c *gin.Context) { c.Set("perms", []int{1, 2, 3}) }, + key: "perms", + wantVal: []string{}, + wantOK: false, + }, + { + name: "empty array", + setup: func(c *gin.Context) { c.Set("perms", []string{}) }, + key: "perms", + wantVal: []string{}, + wantOK: true, + }, + { + name: "nil array", + setup: func(c *gin.Context) { c.Set("perms", []string(nil)) }, + key: "perms", + wantVal: nil, + wantOK: true, + }, + { + name: "single element", + setup: func(c *gin.Context) { c.Set("perms", []string{"admin"}) }, + key: "perms", + wantVal: []string{"admin"}, + wantOK: true, + }, + { + name: "many elements", + setup: func(c *gin.Context) { c.Set("perms", []string{"a", "b", "c", "d", "e"}) }, + key: "perms", + wantVal: []string{"a", "b", "c", "d", "e"}, + wantOK: true, + }, + { + name: "different key", + setup: func(c *gin.Context) { c.Set("other", []string{"val"}) }, + key: "perms", + wantVal: []string{}, + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + tt.setup(c) + val, ok := GetStringArray(c, tt.key) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.wantVal, val) + }) + } +} + +func TestGetStringFromSession(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(session sessions.Session) + key string + wantVal string + wantOK bool + }{ + { + name: "found", + setup: func(s sessions.Session) { + s.Set("token", "abc123") + _ = s.Save() + }, + key: "token", + wantVal: "abc123", + wantOK: true, + }, + { + name: "missing", + setup: func(s sessions.Session) {}, + key: "token", + wantVal: "", + wantOK: false, + }, + { + name: "wrong type int", + setup: func(s sessions.Session) { + s.Set("token", 999) + _ = s.Save() + }, + key: "token", + wantVal: "", + wantOK: false, + }, + { + name: "wrong type bool", + setup: func(s sessions.Session) { + s.Set("token", true) + _ = s.Save() + }, + key: "token", + wantVal: "", + wantOK: false, + }, + { + name: "empty string", + setup: func(s sessions.Session) { + s.Set("token", "") + _ = s.Save() + }, + key: "token", + wantVal: "", + wantOK: true, + }, + { + name: "different key", + setup: func(s sessions.Session) { + s.Set("other", "value") + _ = s.Save() + }, + key: "token", + wantVal: "", + wantOK: false, + }, + { + name: "multiple values in session", + setup: func(s sessions.Session) { + s.Set("token", "abc123") + s.Set("user", "alice") + s.Set("role", "admin") + _ = s.Save() + }, + key: "token", + wantVal: "abc123", + wantOK: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + store := cookie.NewStore([]byte("test-secret")) + router := gin.New() + router.Use(sessions.Sessions("test", store)) + + var val string + var ok bool + + router.GET("/test", func(c *gin.Context) { + session := sessions.Default(c) + tt.setup(session) + val, ok = GetStringFromSession(c, tt.key) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.wantVal, val) + }) + } +} + +func TestMultipleValuesInContext(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + + c.Set("int64_val", int64(123)) + c.Set("uint64_val", uint64(456)) + c.Set("string_val", "test") + c.Set("array_val", []string{"a", "b"}) + + // Verify all values are independently accessible + intVal, ok := GetInt64(c, "int64_val") + assert.True(t, ok) + assert.Equal(t, int64(123), intVal) + + uintVal, ok := GetUint64(c, "uint64_val") + assert.True(t, ok) + assert.Equal(t, uint64(456), uintVal) + + strVal, ok := GetString(c, "string_val") + assert.True(t, ok) + assert.Equal(t, "test", strVal) + + arrVal, ok := GetStringArray(c, "array_val") + assert.True(t, ok) + assert.Equal(t, []string{"a", "b"}, arrVal) +} + +func TestContextOverwrite(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + + // Set initial value + c.Set("key", "original") + val, ok := GetString(c, "key") + assert.True(t, ok) + assert.Equal(t, "original", val) + + // Overwrite with new value + c.Set("key", "updated") + val, ok = GetString(c, "key") + assert.True(t, ok) + assert.Equal(t, "updated", val) +} + +func TestContextTypeChange(t *testing.T) { + t.Parallel() + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + + // Set as string + c.Set("value", "123") + strVal, ok := GetString(c, "value") + assert.True(t, ok) + assert.Equal(t, "123", strVal) + + // Try to get as int64 - should fail + intVal, ok := GetInt64(c, "value") + assert.False(t, ok) + assert.Equal(t, int64(0), intVal) + + // Overwrite with int64 + c.Set("value", int64(123)) + intVal, ok = GetInt64(c, "value") + assert.True(t, ok) + assert.Equal(t, int64(123), intVal) +} diff --git a/backend/pkg/server/response/http_test.go b/backend/pkg/server/response/http_test.go new file mode 100644 index 00000000..f36cc804 --- /dev/null +++ b/backend/pkg/server/response/http_test.go @@ -0,0 +1,434 @@ +package response + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "pentagi/pkg/version" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestNewHttpError(t *testing.T) { + t.Parallel() + + err := NewHttpError(404, "NotFound", "resource not found") + assert.Equal(t, 404, err.HttpCode()) + assert.Equal(t, "NotFound", err.Code()) + assert.Equal(t, "resource not found", err.Msg()) +} + +func TestHttpError_Error(t *testing.T) { + t.Parallel() + + err := NewHttpError(500, "Internal", "something broke") + assert.Equal(t, "Internal: something broke", err.Error()) +} + +func TestHttpError_ImplementsError(t *testing.T) { + t.Parallel() + + var err error = NewHttpError(400, "Bad", "bad request") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Bad") +} + +func TestPredefinedErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err *HttpError + httpCode int + code string + }{ + // General errors + {"ErrInternal", ErrInternal, 500, "Internal"}, + {"ErrInternalDBNotFound", ErrInternalDBNotFound, 500, "Internal.DBNotFound"}, + {"ErrInternalServiceNotFound", ErrInternalServiceNotFound, 500, "Internal.ServiceNotFound"}, + {"ErrInternalDBEncryptorNotFound", ErrInternalDBEncryptorNotFound, 500, "Internal.DBEncryptorNotFound"}, + {"ErrNotPermitted", ErrNotPermitted, 403, "NotPermitted"}, + {"ErrAuthRequired", ErrAuthRequired, 403, "AuthRequired"}, + {"ErrLocalUserRequired", ErrLocalUserRequired, 403, "LocalUserRequired"}, + {"ErrPrivilegesRequired", ErrPrivilegesRequired, 403, "PrivilegesRequired"}, + {"ErrAdminRequired", ErrAdminRequired, 403, "AdminRequired"}, + {"ErrSuperRequired", ErrSuperRequired, 403, "SuperRequired"}, + + // Auth errors + {"ErrAuthInvalidLoginRequest", ErrAuthInvalidLoginRequest, 400, "Auth.InvalidLoginRequest"}, + {"ErrAuthInvalidAuthorizeQuery", ErrAuthInvalidAuthorizeQuery, 400, "Auth.InvalidAuthorizeQuery"}, + {"ErrAuthInvalidLoginCallbackRequest", ErrAuthInvalidLoginCallbackRequest, 400, "Auth.InvalidLoginCallbackRequest"}, + {"ErrAuthInvalidAuthorizationState", ErrAuthInvalidAuthorizationState, 400, "Auth.InvalidAuthorizationState"}, + {"ErrAuthInvalidSwitchServiceHash", ErrAuthInvalidSwitchServiceHash, 400, "Auth.InvalidSwitchServiceHash"}, + {"ErrAuthInvalidAuthorizationNonce", ErrAuthInvalidAuthorizationNonce, 400, "Auth.InvalidAuthorizationNonce"}, + {"ErrAuthInvalidCredentials", ErrAuthInvalidCredentials, 401, "Auth.InvalidCredentials"}, + {"ErrAuthInvalidUserData", ErrAuthInvalidUserData, 500, "Auth.InvalidUserData"}, + {"ErrAuthInactiveUser", ErrAuthInactiveUser, 403, "Auth.InactiveUser"}, + {"ErrAuthExchangeTokenFail", ErrAuthExchangeTokenFail, 403, "Auth.ExchangeTokenFail"}, + {"ErrAuthTokenExpired", ErrAuthTokenExpired, 403, "Auth.TokenExpired"}, + {"ErrAuthVerificationTokenFail", ErrAuthVerificationTokenFail, 403, "Auth.VerificationTokenFail"}, + {"ErrAuthInvalidServiceData", ErrAuthInvalidServiceData, 500, "Auth.InvalidServiceData"}, + {"ErrAuthInvalidTenantData", ErrAuthInvalidTenantData, 500, "Auth.InvalidTenantData"}, + + // Info errors + {"ErrInfoUserNotFound", ErrInfoUserNotFound, 404, "Info.UserNotFound"}, + {"ErrInfoInvalidUserData", ErrInfoInvalidUserData, 500, "Info.InvalidUserData"}, + {"ErrInfoInvalidServiceData", ErrInfoInvalidServiceData, 500, "Info.InvalidServiceData"}, + + // Users errors + {"ErrUsersNotFound", ErrUsersNotFound, 404, "Users.NotFound"}, + {"ErrUsersInvalidData", ErrUsersInvalidData, 500, "Users.InvalidData"}, + {"ErrUsersInvalidRequest", ErrUsersInvalidRequest, 400, "Users.InvalidRequest"}, + {"ErrChangePasswordCurrentUserInvalidPassword", ErrChangePasswordCurrentUserInvalidPassword, 400, "Users.ChangePasswordCurrentUser.InvalidPassword"}, + {"ErrChangePasswordCurrentUserInvalidCurrentPassword", ErrChangePasswordCurrentUserInvalidCurrentPassword, 403, "Users.ChangePasswordCurrentUser.InvalidCurrentPassword"}, + {"ErrChangePasswordCurrentUserInvalidNewPassword", ErrChangePasswordCurrentUserInvalidNewPassword, 400, "Users.ChangePasswordCurrentUser.InvalidNewPassword"}, + {"ErrGetUserModelsNotFound", ErrGetUserModelsNotFound, 404, "Users.GetUser.ModelsNotFound"}, + {"ErrCreateUserInvalidUser", ErrCreateUserInvalidUser, 400, "Users.CreateUser.InvalidUser"}, + {"ErrPatchUserModelsNotFound", ErrPatchUserModelsNotFound, 404, "Users.PatchUser.ModelsNotFound"}, + {"ErrDeleteUserModelsNotFound", ErrDeleteUserModelsNotFound, 404, "Users.DeleteUser.ModelsNotFound"}, + + // Roles errors + {"ErrRolesInvalidRequest", ErrRolesInvalidRequest, 400, "Roles.InvalidRequest"}, + {"ErrRolesInvalidData", ErrRolesInvalidData, 500, "Roles.InvalidData"}, + {"ErrRolesNotFound", ErrRolesNotFound, 404, "Roles.NotFound"}, + + // Prompts errors + {"ErrPromptsInvalidRequest", ErrPromptsInvalidRequest, 400, "Prompts.InvalidRequest"}, + {"ErrPromptsInvalidData", ErrPromptsInvalidData, 500, "Prompts.InvalidData"}, + {"ErrPromptsNotFound", ErrPromptsNotFound, 404, "Prompts.NotFound"}, + + // Screenshots errors + {"ErrScreenshotsInvalidRequest", ErrScreenshotsInvalidRequest, 400, "Screenshots.InvalidRequest"}, + {"ErrScreenshotsNotFound", ErrScreenshotsNotFound, 404, "Screenshots.NotFound"}, + {"ErrScreenshotsInvalidData", ErrScreenshotsInvalidData, 500, "Screenshots.InvalidData"}, + + // Containers errors + {"ErrContainersInvalidRequest", ErrContainersInvalidRequest, 400, "Containers.InvalidRequest"}, + {"ErrContainersNotFound", ErrContainersNotFound, 404, "Containers.NotFound"}, + {"ErrContainersInvalidData", ErrContainersInvalidData, 500, "Containers.InvalidData"}, + + // Agentlogs errors + {"ErrAgentlogsInvalidRequest", ErrAgentlogsInvalidRequest, 400, "Agentlogs.InvalidRequest"}, + {"ErrAgentlogsInvalidData", ErrAgentlogsInvalidData, 500, "Agentlogs.InvalidData"}, + + // Assistantlogs errors + {"ErrAssistantlogsInvalidRequest", ErrAssistantlogsInvalidRequest, 400, "Assistantlogs.InvalidRequest"}, + {"ErrAssistantlogsInvalidData", ErrAssistantlogsInvalidData, 500, "Assistantlogs.InvalidData"}, + + // Msglogs errors + {"ErrMsglogsInvalidRequest", ErrMsglogsInvalidRequest, 400, "Msglogs.InvalidRequest"}, + {"ErrMsglogsInvalidData", ErrMsglogsInvalidData, 500, "Msglogs.InvalidData"}, + + // Searchlogs errors + {"ErrSearchlogsInvalidRequest", ErrSearchlogsInvalidRequest, 400, "Searchlogs.InvalidRequest"}, + {"ErrSearchlogsInvalidData", ErrSearchlogsInvalidData, 500, "Searchlogs.InvalidData"}, + + // Termlogs errors + {"ErrTermlogsInvalidRequest", ErrTermlogsInvalidRequest, 400, "Termlogs.InvalidRequest"}, + {"ErrTermlogsInvalidData", ErrTermlogsInvalidData, 500, "Termlogs.InvalidData"}, + + // Vecstorelogs errors + {"ErrVecstorelogsInvalidRequest", ErrVecstorelogsInvalidRequest, 400, "Vecstorelogs.InvalidRequest"}, + {"ErrVecstorelogsInvalidData", ErrVecstorelogsInvalidData, 500, "Vecstorelogs.InvalidData"}, + + // Flows errors + {"ErrFlowsInvalidRequest", ErrFlowsInvalidRequest, 400, "Flows.InvalidRequest"}, + {"ErrFlowsNotFound", ErrFlowsNotFound, 404, "Flows.NotFound"}, + {"ErrFlowsInvalidData", ErrFlowsInvalidData, 500, "Flows.InvalidData"}, + + // Tasks errors + {"ErrTasksInvalidRequest", ErrTasksInvalidRequest, 400, "Tasks.InvalidRequest"}, + {"ErrTasksNotFound", ErrTasksNotFound, 404, "Tasks.NotFound"}, + {"ErrTasksInvalidData", ErrTasksInvalidData, 500, "Tasks.InvalidData"}, + + // Subtasks errors + {"ErrSubtasksInvalidRequest", ErrSubtasksInvalidRequest, 400, "Subtasks.InvalidRequest"}, + {"ErrSubtasksNotFound", ErrSubtasksNotFound, 404, "Subtasks.NotFound"}, + {"ErrSubtasksInvalidData", ErrSubtasksInvalidData, 500, "Subtasks.InvalidData"}, + + // Assistants errors + {"ErrAssistantsInvalidRequest", ErrAssistantsInvalidRequest, 400, "Assistants.InvalidRequest"}, + {"ErrAssistantsNotFound", ErrAssistantsNotFound, 404, "Assistants.NotFound"}, + {"ErrAssistantsInvalidData", ErrAssistantsInvalidData, 500, "Assistants.InvalidData"}, + + // Tokens errors + {"ErrTokenCreationDisabled", ErrTokenCreationDisabled, 400, "Token.CreationDisabled"}, + {"ErrTokenNotFound", ErrTokenNotFound, 404, "Token.NotFound"}, + {"ErrTokenUnauthorized", ErrTokenUnauthorized, 403, "Token.Unauthorized"}, + {"ErrTokenInvalidRequest", ErrTokenInvalidRequest, 400, "Token.InvalidRequest"}, + {"ErrTokenInvalidData", ErrTokenInvalidData, 500, "Token.InvalidData"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tt.httpCode, tt.err.HttpCode()) + assert.Equal(t, tt.code, tt.err.Code()) + assert.NotEmpty(t, tt.err.Msg()) + assert.NotEmpty(t, tt.err.Error()) + }) + } +} + +func TestSuccessResponse(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + data := map[string]string{"id": "123"} + Success(c, http.StatusOK, data) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + assert.Equal(t, "success", body["status"]) + assert.NotNil(t, body["data"]) +} + +func TestSuccessResponse_Created(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Success(c, http.StatusCreated, gin.H{"name": "test"}) + + assert.Equal(t, http.StatusCreated, w.Code) +} + +func TestErrorResponse(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) + + Error(c, ErrInternal, errors.New("db connection failed")) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + assert.Equal(t, "error", body["status"]) + assert.Equal(t, "Internal", body["code"]) + assert.Equal(t, "internal server error", body["msg"]) +} + +func TestErrorResponse_DevMode(t *testing.T) { + // Save original version and restore after test + oldVer := version.PackageVer + defer func() { version.PackageVer = oldVer }() + + // Enable dev mode + version.PackageVer = "" + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) + + originalErr := errors.New("detailed error info") + Error(c, ErrInternal, originalErr) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + + // In dev mode, original error should be included + assert.Equal(t, "detailed error info", body["error"]) +} + +func TestErrorResponse_ProductionMode(t *testing.T) { + // Save original version and restore after test + oldVer := version.PackageVer + defer func() { version.PackageVer = oldVer }() + + // Set production mode + version.PackageVer = "1.0.0" + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) + + Error(c, ErrInternal, errors.New("should not appear")) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + + // In production mode, original error should NOT be included + _, hasError := body["error"] + assert.False(t, hasError) +} + +func TestErrorResponse_NilOriginalError(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) + + Error(c, ErrNotPermitted, nil) + + assert.Equal(t, http.StatusForbidden, w.Code) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + assert.Equal(t, "NotPermitted", body["code"]) +} + +func TestHttpError_MultipleInstancesIndependent(t *testing.T) { + t.Parallel() + + err1 := NewHttpError(404, "NotFound", "resource 1 not found") + err2 := NewHttpError(404, "NotFound", "resource 2 not found") + + // Verify they are independent instances + assert.NotEqual(t, err1.Msg(), err2.Msg()) + assert.Equal(t, err1.Code(), err2.Code()) + assert.Equal(t, err1.HttpCode(), err2.HttpCode()) +} + +func TestSuccessResponse_EmptyData(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Success(c, http.StatusOK, nil) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + assert.Equal(t, "success", body["status"]) + assert.Nil(t, body["data"]) +} + +func TestSuccessResponse_ComplexData(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + data := gin.H{ + "users": []gin.H{ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"}, + }, + "count": 2, + "meta": gin.H{ + "page": 1, + "total": 100, + }, + } + + Success(c, http.StatusOK, data) + + assert.Equal(t, http.StatusOK, w.Code) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + assert.Equal(t, "success", body["status"]) + + responseData, ok := body["data"].(map[string]any) + require.True(t, ok) + assert.Equal(t, float64(2), responseData["count"]) +} + +func TestErrorResponse_DifferentHttpCodes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err *HttpError + expected int + }{ + {"400 Bad Request", ErrPromptsInvalidRequest, http.StatusBadRequest}, + {"401 Unauthorized", ErrAuthInvalidCredentials, http.StatusUnauthorized}, + {"403 Forbidden", ErrNotPermitted, http.StatusForbidden}, + {"404 Not Found", ErrUsersNotFound, http.StatusNotFound}, + {"500 Internal", ErrInternal, http.StatusInternalServerError}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) + + Error(c, tt.err, nil) + + assert.Equal(t, tt.expected, w.Code) + }) + } +} + +func TestErrorResponse_ResponseStructure(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) + + Error(c, ErrUsersNotFound, nil) + + var body map[string]any + err := json.Unmarshal(w.Body.Bytes(), &body) + require.NoError(t, err) + + // Verify required fields + assert.Equal(t, "error", body["status"]) + assert.Equal(t, "Users.NotFound", body["code"]) + assert.Equal(t, "user not found", body["msg"]) + + // Verify error field is not present in non-dev mode + _, hasError := body["error"] + assert.False(t, hasError) +} + +func TestSuccessResponse_StatusCodes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + statusCode int + expectedCode int + }{ + {"200 OK", http.StatusOK, 200}, + {"201 Created", http.StatusCreated, 201}, + {"202 Accepted", http.StatusAccepted, 202}, + {"204 No Content", http.StatusNoContent, 204}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Success(c, tt.statusCode, gin.H{"test": "data"}) + + assert.Equal(t, tt.expectedCode, w.Code) + }) + } +} diff --git a/backend/pkg/system/utils.go b/backend/pkg/system/utils.go index 18909c0d..db5402db 100644 --- a/backend/pkg/system/utils.go +++ b/backend/pkg/system/utils.go @@ -8,10 +8,16 @@ import ( "net/http" "net/url" "os" + "time" "pentagi/pkg/config" ) +const ( + // defaultHTTPClientTimeout is the fallback timeout when no config is provided. + defaultHTTPClientTimeout = 10 * time.Minute +) + func getHostname() string { hn, err := os.Hostname() if err != nil { @@ -65,7 +71,9 @@ func GetHTTPClient(cfg *config.Config) (*http.Client, error) { var httpClient *http.Client if cfg == nil { - return http.DefaultClient, nil + return &http.Client{ + Timeout: defaultHTTPClientTimeout, + }, nil } rootCAPool, err := GetSystemCertPool(cfg) @@ -73,8 +81,15 @@ func GetHTTPClient(cfg *config.Config) (*http.Client, error) { return nil, err } + // Convert timeout from config (in seconds) to time.Duration + // 0 = no timeout (unlimited), >0 = timeout in seconds + // Default value (600) is automatically set in config.go via envDefault:"600" tag + // when HTTP_CLIENT_TIMEOUT environment variable is not set + timeout := max(time.Duration(cfg.HTTPClientTimeout)*time.Second, 0) + if cfg.ProxyURL != "" { httpClient = &http.Client{ + Timeout: timeout, Transport: &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(cfg.ProxyURL) @@ -87,6 +102,7 @@ func GetHTTPClient(cfg *config.Config) (*http.Client, error) { } } else { httpClient = &http.Client{ + Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: rootCAPool, diff --git a/backend/pkg/system/utils_test.go b/backend/pkg/system/utils_test.go index 0f36fbe2..5068759d 100644 --- a/backend/pkg/system/utils_test.go +++ b/backend/pkg/system/utils_test.go @@ -211,6 +211,7 @@ func createTestConfig(caPath string, insecure bool, proxyURL string) *config.Con ExternalSSLCAPath: caPath, ExternalSSLInsecure: insecure, ProxyURL: proxyURL, + HTTPClientTimeout: 600, // default 10 minutes } } @@ -635,6 +636,115 @@ func TestHTTPClient_RealConnection_MultipleRootCAs(t *testing.T) { } } +func TestGetHTTPClient_NilConfig(t *testing.T) { + client, err := GetHTTPClient(nil) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if client == nil { + t.Fatal("expected non-nil HTTP client") + } + + if client.Timeout != defaultHTTPClientTimeout { + t.Errorf("expected default timeout %v, got %v", defaultHTTPClientTimeout, client.Timeout) + } +} + +func TestGetHTTPClient_DefaultTimeout(t *testing.T) { + cfg := createTestConfig("", false, "") + + client, err := GetHTTPClient(cfg) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + expected := 600 * time.Second + if client.Timeout != expected { + t.Errorf("expected timeout %v, got %v", expected, client.Timeout) + } +} + +func TestGetHTTPClient_CustomTimeout(t *testing.T) { + cfg := &config.Config{ + HTTPClientTimeout: 120, + } + + client, err := GetHTTPClient(cfg) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + expected := 120 * time.Second + if client.Timeout != expected { + t.Errorf("expected timeout %v, got %v", expected, client.Timeout) + } +} + +func TestGetHTTPClient_ZeroTimeoutMeansNoTimeout(t *testing.T) { + cfg := &config.Config{ + HTTPClientTimeout: 0, + } + + client, err := GetHTTPClient(cfg) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if client.Timeout != 0 { + t.Errorf("expected no timeout (0) when explicitly set to 0, got %v", client.Timeout) + } +} + +func TestGetHTTPClient_TimeoutWithProxy(t *testing.T) { + cfg := &config.Config{ + HTTPClientTimeout: 300, + ProxyURL: "http://proxy.example.com:8080", + } + + client, err := GetHTTPClient(cfg) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + expected := 300 * time.Second + if client.Timeout != expected { + t.Errorf("expected timeout %v with proxy, got %v", expected, client.Timeout) + } +} + +func TestGetHTTPClient_NegativeTimeoutClampsToZero(t *testing.T) { + cfg := &config.Config{ + HTTPClientTimeout: -100, + } + + client, err := GetHTTPClient(cfg) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + // Negative values are clamped to 0 by max() function + if client.Timeout != 0 { + t.Errorf("expected timeout 0 (clamped from negative), got %v", client.Timeout) + } +} + +func TestGetHTTPClient_LargeTimeout(t *testing.T) { + cfg := &config.Config{ + HTTPClientTimeout: 3600, // 1 hour + } + + client, err := GetHTTPClient(cfg) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + expected := 3600 * time.Second + if client.Timeout != expected { + t.Errorf("expected timeout %v, got %v", expected, client.Timeout) + } +} + func TestHTTPClient_RealConnection_InsecureMode(t *testing.T) { certs, err := generateTestCerts() if err != nil { diff --git a/backend/pkg/templates/prompts/tool_call_id_collector.tmpl b/backend/pkg/templates/prompts/tool_call_id_collector.tmpl index b450eba4..1185fef7 100644 --- a/backend/pkg/templates/prompts/tool_call_id_collector.tmpl +++ b/backend/pkg/templates/prompts/tool_call_id_collector.tmpl @@ -14,9 +14,14 @@ DO NOT: - Describe what you will do instead of doing it - Explain your reasoning before calling the function - Ask questions or request clarification +- End your turn without making function calls +- Provide a text response as a substitute for function calls REQUIRED ACTION: Call {{.FunctionName}} function exactly three times in parallel with value=42. +COMPLETION REQUIREMENT: Your work is NOT complete until you have successfully invoked the {{.FunctionName}} function exactly three times. +DO NOT terminate, finish, or end your response without making these function calls. + Any response that does not include exactly three function calls will be treated as a CRITICAL ERROR. diff --git a/backend/pkg/templates/prompts/tool_call_id_detector.tmpl b/backend/pkg/templates/prompts/tool_call_id_detector.tmpl index 1e1360ab..bb2f2bc2 100644 --- a/backend/pkg/templates/prompts/tool_call_id_detector.tmpl +++ b/backend/pkg/templates/prompts/tool_call_id_detector.tmpl @@ -14,9 +14,14 @@ DO NOT: - Describe your analysis in plain text - Explain your reasoning before calling the function - Ask questions or request clarification +- End your turn without making a function call +- Provide a text response as a substitute for a function call REQUIRED ACTION: Call {{.FunctionName}} function immediately with the detected pattern template. +COMPLETION REQUIREMENT: Your work is NOT complete until you have successfully invoked the {{.FunctionName}} function. +DO NOT terminate, finish, or end your response without making this function call. + Any response that does not include a function call will be treated as a CRITICAL ERROR and will force a retry. diff --git a/backend/pkg/terminal/output_test.go b/backend/pkg/terminal/output_test.go new file mode 100644 index 00000000..1abe5caf --- /dev/null +++ b/backend/pkg/terminal/output_test.go @@ -0,0 +1,316 @@ +package terminal + +import ( + "context" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsMarkdownContent_Headers(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + {"h1 prefix", "# Title", true}, + {"h2 prefix", "## Subtitle", true}, + {"h3 in body", "some text\n# Header", true}, + {"plain text", "just some regular text", false}, + {"empty string", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsMarkdownContent(tt.input)) + }) + } +} + +func TestIsMarkdownContent_CodeBlocks(t *testing.T) { + assert.True(t, IsMarkdownContent("```go\nfmt.Println()\n```")) +} + +func TestIsMarkdownContent_Bold(t *testing.T) { + assert.True(t, IsMarkdownContent("this is **bold** text")) +} + +func TestIsMarkdownContent_Links(t *testing.T) { + assert.True(t, IsMarkdownContent("click [here](https://example.com)")) +} + +func TestIsMarkdownContent_Lists(t *testing.T) { + assert.True(t, IsMarkdownContent("items:\n- first\n- second")) +} + +func TestIsMarkdownContent_PlainText(t *testing.T) { + assert.False(t, IsMarkdownContent("no markdown here at all")) + assert.False(t, IsMarkdownContent("single line")) +} + +func TestIsMarkdownContent_EdgeCases(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + {"single bracket", "[", false}, + {"incomplete link", "[text", false}, + {"star without pair", "this has * one star", false}, + {"backtick without triple", "single ` backtick", false}, + {"hyphen without list", "text - not a list", false}, + {"complete link", "[link](url)", true}, + {"double star pair", "text **bold** text", true}, + {"triple backticks", "```code```", true}, + {"proper list", "item\n- list item", true}, + {"multiple markdown features", "# Title\n\n**bold** and [link](url)", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsMarkdownContent(tt.input)) + }) + } +} + +func TestInteractivePromptContext_ReadsInput(t *testing.T) { + reader := strings.NewReader("hello world\n") + + result, err := InteractivePromptContext(t.Context(), "Enter", reader) + require.NoError(t, err) + assert.Equal(t, "hello world", result) +} + +func TestInteractivePromptContext_TrimsWhitespace(t *testing.T) { + reader := strings.NewReader(" trimmed \n") + + result, err := InteractivePromptContext(t.Context(), "Enter", reader) + require.NoError(t, err) + assert.Equal(t, "trimmed", result) +} + +func TestInteractivePromptContext_CancelledContext(t *testing.T) { + pr, pw := io.Pipe() + defer pw.Close() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + _, err := InteractivePromptContext(ctx, "Enter", pr) + require.ErrorIs(t, err, context.Canceled) +} + +func TestGetYesNoInputContext_Yes(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"lowercase y", "y\n"}, + {"lowercase yes", "yes\n"}, + {"uppercase Y", "Y\n"}, + {"uppercase YES", "YES\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := strings.NewReader(tt.input) + result, err := GetYesNoInputContext(t.Context(), "Confirm?", reader) + require.NoError(t, err) + assert.True(t, result) + }) + } +} + +func TestGetYesNoInputContext_No(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"lowercase n", "n\n"}, + {"lowercase no", "no\n"}, + {"uppercase N", "N\n"}, + {"uppercase NO", "NO\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := strings.NewReader(tt.input) + result, err := GetYesNoInputContext(t.Context(), "Confirm?", reader) + require.NoError(t, err) + assert.False(t, result) + }) + } +} + +func TestGetYesNoInputContext_CancelledContext(t *testing.T) { + pr, pw := io.Pipe() + defer pw.Close() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + _, err := GetYesNoInputContext(ctx, "Confirm?", pr) + require.ErrorIs(t, err, context.Canceled) +} + +func TestGetYesNoInputContext_InvalidInput(t *testing.T) { + // Test with invalid input followed by EOF + reader := strings.NewReader("invalid\n") + + _, err := GetYesNoInputContext(t.Context(), "Confirm?", reader) + require.Error(t, err) + assert.ErrorIs(t, err, io.EOF) +} + +func TestGetYesNoInputContext_EOFError(t *testing.T) { + reader := strings.NewReader("") // EOF immediately + + _, err := GetYesNoInputContext(t.Context(), "Confirm?", reader) + require.Error(t, err) + assert.ErrorIs(t, err, io.EOF) +} + +func TestInteractivePromptContext_EOFError(t *testing.T) { + reader := strings.NewReader("") // EOF immediately + + _, err := InteractivePromptContext(t.Context(), "Enter", reader) + require.Error(t, err) + assert.ErrorIs(t, err, io.EOF) +} + +func TestInteractivePromptContext_EmptyInput(t *testing.T) { + reader := strings.NewReader("\n") // Just newline + + result, err := InteractivePromptContext(t.Context(), "Enter", reader) + require.NoError(t, err) + assert.Equal(t, "", result) +} + +func TestPrintJSON_ValidData(t *testing.T) { + data := map[string]string{"key": "value"} + assert.NotPanics(t, func() { + PrintJSON(data) + }) +} + +func TestPrintJSON_InvalidData(t *testing.T) { + assert.NotPanics(t, func() { + PrintJSON(make(chan int)) + }) +} + +func TestPrintJSON_ComplexData(t *testing.T) { + data := map[string]interface{}{ + "string": "value", + "number": 42, + "bool": true, + "array": []string{"a", "b", "c"}, + "nested": map[string]int{"x": 1, "y": 2}, + } + + assert.NotPanics(t, func() { + PrintJSON(data) + }) +} + +func TestPrintJSON_NilData(t *testing.T) { + assert.NotPanics(t, func() { + PrintJSON(nil) + }) +} + +func TestRenderMarkdown_Empty(t *testing.T) { + assert.NotPanics(t, func() { + RenderMarkdown("") + }) +} + +func TestRenderMarkdown_ValidContent(t *testing.T) { + assert.NotPanics(t, func() { + RenderMarkdown("# Hello\n\nThis is **bold**") + }) +} + +func TestPrintResult_PlainText(t *testing.T) { + assert.NotPanics(t, func() { + PrintResult("plain text output") + }) +} + +func TestPrintResult_MarkdownContent(t *testing.T) { + assert.NotPanics(t, func() { + PrintResult("# Header\n\nSome **bold** text") + }) +} + +func TestPrintResultWithKey_PlainText(t *testing.T) { + // PrintResultWithKey uses colored output for key, which goes to stderr + assert.NotPanics(t, func() { + PrintResultWithKey("Result", "plain text output") + }) +} + +func TestPrintResultWithKey_MarkdownContent(t *testing.T) { + // PrintResultWithKey uses colored output for key, which goes to stderr + assert.NotPanics(t, func() { + PrintResultWithKey("Analysis", "# Findings\n\n- **Critical**: Issue found") + }) +} + +func TestColoredOutputFunctions_DoNotPanic(t *testing.T) { + // Color output functions use fatih/color which writes to a custom output + // that may behave differently in test environments. We verify they don't panic. + tests := []struct { + name string + fn func(string, ...interface{}) + }{ + {"Info", Info}, + {"Success", Success}, + {"Error", Error}, + {"Warning", Warning}, + {"PrintInfo", PrintInfo}, + {"PrintSuccess", PrintSuccess}, + {"PrintError", PrintError}, + {"PrintWarning", PrintWarning}, + {"PrintMock", PrintMock}, + {"PrintHeader", func(s string, _ ...interface{}) { PrintHeader(s) }}, + {"PrintValueFormat", PrintValueFormat}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.NotPanics(t, func() { + tt.fn("test message") + }) + }) + } +} + +func TestPrintKeyValue_DoesNotPanic(t *testing.T) { + assert.NotPanics(t, func() { + PrintKeyValue("Name", "PentAGI") + }) +} + +func TestPrintKeyValueFormat_DoesNotPanic(t *testing.T) { + assert.NotPanics(t, func() { + PrintKeyValueFormat("Score", "%d%%", 95) + }) +} + +func TestPrintSeparators_DoNotPanic(t *testing.T) { + t.Run("thin separator", func(t *testing.T) { + assert.NotPanics(t, func() { + PrintThinSeparator() + }) + }) + + t.Run("thick separator", func(t *testing.T) { + assert.NotPanics(t, func() { + PrintThickSeparator() + }) + }) +} diff --git a/backend/pkg/tools/terminal_context_test.go b/backend/pkg/tools/terminal_context_test.go deleted file mode 100644 index 69fffa80..00000000 --- a/backend/pkg/tools/terminal_context_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package tools - -import ( - "bufio" - "context" - "io" - "net" - "testing" - "time" - - "pentagi/pkg/database" - "pentagi/pkg/docker" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/stretchr/testify/assert" -) - -// contextTestTermLogProvider implements TermLogProvider for context tests. -type contextTestTermLogProvider struct{} - -func (m *contextTestTermLogProvider) PutMsg(_ context.Context, _ database.TermlogType, _ string, - _ int64, _, _ *int64) (int64, error) { - return 1, nil -} - -var _ TermLogProvider = (*contextTestTermLogProvider)(nil) - -// contextAwareMockDockerClient tracks whether the context was canceled -// when getExecResult runs, proving context.WithoutCancel works. -type contextAwareMockDockerClient struct { - isRunning bool - execCreateResp container.ExecCreateResponse - attachOutput []byte - attachDelay time.Duration - inspectResp container.ExecInspect - - // Set by ContainerExecAttach to track if ctx was canceled during attach - ctxWasCanceled bool -} - -func (m *contextAwareMockDockerClient) SpawnContainer(_ context.Context, _ string, _ database.ContainerType, - _ int64, _ *container.Config, _ *container.HostConfig) (database.Container, error) { - return database.Container{}, nil -} -func (m *contextAwareMockDockerClient) StopContainer(_ context.Context, _ string, _ int64) error { - return nil -} -func (m *contextAwareMockDockerClient) DeleteContainer(_ context.Context, _ string, _ int64) error { - return nil -} -func (m *contextAwareMockDockerClient) IsContainerRunning(_ context.Context, _ string) (bool, error) { - return m.isRunning, nil -} -func (m *contextAwareMockDockerClient) ContainerExecCreate(_ context.Context, _ string, _ container.ExecOptions) (container.ExecCreateResponse, error) { - return m.execCreateResp, nil -} -func (m *contextAwareMockDockerClient) ContainerExecAttach(ctx context.Context, _ string, _ container.ExecAttachOptions) (types.HijackedResponse, error) { - // Wait for the configured delay, simulating a long-running command - if m.attachDelay > 0 { - select { - case <-time.After(m.attachDelay): - // Command completed normally - case <-ctx.Done(): - // Context was canceled -- this is the bug behavior (without WithoutCancel) - m.ctxWasCanceled = true - return types.HijackedResponse{}, ctx.Err() - } - } - - // Check if context was already canceled by the time we get here - select { - case <-ctx.Done(): - m.ctxWasCanceled = true - return types.HijackedResponse{}, ctx.Err() - default: - } - - pr, pw := net.Pipe() - go func() { - pw.Write(m.attachOutput) - pw.Close() - }() - - return types.HijackedResponse{ - Conn: pr, - Reader: bufio.NewReader(pr), - }, nil -} -func (m *contextAwareMockDockerClient) ContainerExecInspect(_ context.Context, _ string) (container.ExecInspect, error) { - return m.inspectResp, nil -} -func (m *contextAwareMockDockerClient) CopyToContainer(_ context.Context, _ string, _ string, _ io.Reader, _ container.CopyToContainerOptions) error { - return nil -} -func (m *contextAwareMockDockerClient) CopyFromContainer(_ context.Context, _ string, _ string) (io.ReadCloser, container.PathStat, error) { - return io.NopCloser(nil), container.PathStat{}, nil -} -func (m *contextAwareMockDockerClient) Cleanup(_ context.Context) error { return nil } -func (m *contextAwareMockDockerClient) GetDefaultImage() string { return "test-image" } - -var _ docker.DockerClient = (*contextAwareMockDockerClient)(nil) - -func TestExecCommandDetachSurvivesParentCancel(t *testing.T) { - // This test validates the fix for Issue #176: - // Detached commands must NOT be killed when the parent context is canceled. - // - // Before the fix: detached goroutine used parent ctx directly, so when the - // parent was canceled (e.g., agent delegation timeout), ctx.Done() fired - // in getExecResult and killed the background command. - // - // After the fix: context.WithoutCancel(ctx) creates an isolated context - // that preserves values but ignores parent cancellation. - - mock := &contextAwareMockDockerClient{ - isRunning: true, - execCreateResp: container.ExecCreateResponse{ID: "exec-cancel-test"}, - attachOutput: []byte("background result"), - attachDelay: 2 * time.Second, // simulates a long-running command - inspectResp: container.ExecInspect{ExitCode: 0}, - } - - term := &terminal{ - flowID: 1, - containerID: 1, - containerLID: "test-container", - dockerClient: mock, - tlp: &contextTestTermLogProvider{}, - } - - // Create a cancellable parent context - parentCtx, cancel := context.WithCancel(context.Background()) - - // Start ExecCommand with detach=true (returns quickly due to quick check timeout) - output, err := term.ExecCommand(parentCtx, "/work", "long-running-scan", true, 5*time.Minute) - assert.NoError(t, err) - assert.Contains(t, output, "Command started in background") - - // Cancel the parent context -- simulating agent delegation timeout - cancel() - - // Wait enough time for the detached goroutine to complete its work. - // If context.WithoutCancel is working correctly, the goroutine should - // NOT see ctx.Done() and should complete normally after attachDelay. - // If the fix regresses, ctxWasCanceled will be true. - time.Sleep(3 * time.Second) - - assert.False(t, mock.ctxWasCanceled, - "detached goroutine should NOT see parent context cancellation (context.WithoutCancel must be used)") -} - -func TestExecCommandNonDetachRespectsParentCancel(t *testing.T) { - // Counterpart: non-detached commands SHOULD respect parent cancellation. - // This ensures we didn't accidentally apply WithoutCancel to the non-detach path. - - mock := &contextAwareMockDockerClient{ - isRunning: true, - execCreateResp: container.ExecCreateResponse{ID: "exec-nondetach-cancel"}, - attachOutput: []byte("should not complete"), - attachDelay: 5 * time.Second, // longer than cancel delay - inspectResp: container.ExecInspect{ExitCode: 0}, - } - - term := &terminal{ - flowID: 1, - containerID: 1, - containerLID: "test-container", - dockerClient: mock, - tlp: &contextTestTermLogProvider{}, - } - - parentCtx, cancel := context.WithCancel(context.Background()) - - // Cancel after 200ms -- non-detached command should see this - go func() { - time.Sleep(200 * time.Millisecond) - cancel() - }() - - _, err := term.ExecCommand(parentCtx, "/work", "long-command", false, 5*time.Minute) - - // Non-detached command should fail with context error - assert.Error(t, err) - assert.True(t, mock.ctxWasCanceled, - "non-detached command SHOULD see parent context cancellation") -} diff --git a/backend/pkg/tools/terminal_test.go b/backend/pkg/tools/terminal_test.go index e7b1144a..c847e307 100644 --- a/backend/pkg/tools/terminal_test.go +++ b/backend/pkg/tools/terminal_test.go @@ -1,11 +1,192 @@ package tools import ( + "bufio" + "context" "fmt" + "io" + "net" "strings" "testing" + "time" + + "pentagi/pkg/database" + "pentagi/pkg/docker" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/assert" ) +// contextTestTermLogProvider implements TermLogProvider for context tests. +type contextTestTermLogProvider struct{} + +func (m *contextTestTermLogProvider) PutMsg(_ context.Context, _ database.TermlogType, _ string, + _ int64, _, _ *int64) (int64, error) { + return 1, nil +} + +var _ TermLogProvider = (*contextTestTermLogProvider)(nil) + +// contextAwareMockDockerClient tracks whether the context was canceled +// when getExecResult runs, proving context.WithoutCancel works. +type contextAwareMockDockerClient struct { + isRunning bool + execCreateResp container.ExecCreateResponse + attachOutput []byte + attachDelay time.Duration + inspectResp container.ExecInspect + + // Set by ContainerExecAttach to track if ctx was canceled during attach + ctxWasCanceled bool +} + +func (m *contextAwareMockDockerClient) SpawnContainer(_ context.Context, _ string, _ database.ContainerType, + _ int64, _ *container.Config, _ *container.HostConfig) (database.Container, error) { + return database.Container{}, nil +} +func (m *contextAwareMockDockerClient) StopContainer(_ context.Context, _ string, _ int64) error { + return nil +} +func (m *contextAwareMockDockerClient) DeleteContainer(_ context.Context, _ string, _ int64) error { + return nil +} +func (m *contextAwareMockDockerClient) IsContainerRunning(_ context.Context, _ string) (bool, error) { + return m.isRunning, nil +} +func (m *contextAwareMockDockerClient) ContainerExecCreate(_ context.Context, _ string, _ container.ExecOptions) (container.ExecCreateResponse, error) { + return m.execCreateResp, nil +} +func (m *contextAwareMockDockerClient) ContainerExecAttach(ctx context.Context, _ string, _ container.ExecAttachOptions) (types.HijackedResponse, error) { + // Wait for the configured delay, simulating a long-running command + if m.attachDelay > 0 { + select { + case <-time.After(m.attachDelay): + // Command completed normally + case <-ctx.Done(): + // Context was canceled -- this is the bug behavior (without WithoutCancel) + m.ctxWasCanceled = true + return types.HijackedResponse{}, ctx.Err() + } + } + + // Check if context was already canceled by the time we get here + select { + case <-ctx.Done(): + m.ctxWasCanceled = true + return types.HijackedResponse{}, ctx.Err() + default: + } + + pr, pw := net.Pipe() + go func() { + pw.Write(m.attachOutput) + pw.Close() + }() + + return types.HijackedResponse{ + Conn: pr, + Reader: bufio.NewReader(pr), + }, nil +} +func (m *contextAwareMockDockerClient) ContainerExecInspect(_ context.Context, _ string) (container.ExecInspect, error) { + return m.inspectResp, nil +} +func (m *contextAwareMockDockerClient) CopyToContainer(_ context.Context, _ string, _ string, _ io.Reader, _ container.CopyToContainerOptions) error { + return nil +} +func (m *contextAwareMockDockerClient) CopyFromContainer(_ context.Context, _ string, _ string) (io.ReadCloser, container.PathStat, error) { + return io.NopCloser(nil), container.PathStat{}, nil +} +func (m *contextAwareMockDockerClient) Cleanup(_ context.Context) error { return nil } +func (m *contextAwareMockDockerClient) GetDefaultImage() string { return "test-image" } + +var _ docker.DockerClient = (*contextAwareMockDockerClient)(nil) + +func TestExecCommandDetachSurvivesParentCancel(t *testing.T) { + // This test validates the fix for Issue #176: + // Detached commands must NOT be killed when the parent context is canceled. + // + // Before the fix: detached goroutine used parent ctx directly, so when the + // parent was canceled (e.g., agent delegation timeout), ctx.Done() fired + // in getExecResult and killed the background command. + // + // After the fix: context.WithoutCancel(ctx) creates an isolated context + // that preserves values but ignores parent cancellation. + + mock := &contextAwareMockDockerClient{ + isRunning: true, + execCreateResp: container.ExecCreateResponse{ID: "exec-cancel-test"}, + attachOutput: []byte("background result"), + attachDelay: 2 * time.Second, // simulates a long-running command + inspectResp: container.ExecInspect{ExitCode: 0}, + } + + term := &terminal{ + flowID: 1, + containerID: 1, + containerLID: "test-container", + dockerClient: mock, + tlp: &contextTestTermLogProvider{}, + } + + // Create a cancellable parent context + parentCtx, cancel := context.WithCancel(t.Context()) + + // Start ExecCommand with detach=true (returns quickly due to quick check timeout) + output, err := term.ExecCommand(parentCtx, "/work", "long-running-scan", true, 5*time.Minute) + assert.NoError(t, err) + assert.Contains(t, output, "Command started in background") + + // Cancel the parent context -- simulating agent delegation timeout + cancel() + + // Wait enough time for the detached goroutine to complete its work. + // If context.WithoutCancel is working correctly, the goroutine should + // NOT see ctx.Done() and should complete normally after attachDelay. + // If the fix regresses, ctxWasCanceled will be true. + time.Sleep(3 * time.Second) + + assert.False(t, mock.ctxWasCanceled, + "detached goroutine should NOT see parent context cancellation (context.WithoutCancel must be used)") +} + +func TestExecCommandNonDetachRespectsParentCancel(t *testing.T) { + // Counterpart: non-detached commands SHOULD respect parent cancellation. + // This ensures we didn't accidentally apply WithoutCancel to the non-detach path. + + mock := &contextAwareMockDockerClient{ + isRunning: true, + execCreateResp: container.ExecCreateResponse{ID: "exec-nondetach-cancel"}, + attachOutput: []byte("should not complete"), + attachDelay: 5 * time.Second, // longer than cancel delay + inspectResp: container.ExecInspect{ExitCode: 0}, + } + + term := &terminal{ + flowID: 1, + containerID: 1, + containerLID: "test-container", + dockerClient: mock, + tlp: &contextTestTermLogProvider{}, + } + + parentCtx, cancel := context.WithCancel(t.Context()) + + // Cancel after 200ms -- non-detached command should see this + go func() { + time.Sleep(200 * time.Millisecond) + cancel() + }() + + _, err := term.ExecCommand(parentCtx, "/work", "long-command", false, 5*time.Minute) + + // Non-detached command should fail with context error + assert.Error(t, err) + assert.True(t, mock.ctxWasCanceled, + "non-detached command SHOULD see parent context cancellation") +} + func TestPrimaryTerminalName(t *testing.T) { t.Parallel() diff --git a/backend/pkg/version/version_test.go b/backend/pkg/version/version_test.go new file mode 100644 index 00000000..41a3799f --- /dev/null +++ b/backend/pkg/version/version_test.go @@ -0,0 +1,73 @@ +package version + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetBinaryVersion_Default(t *testing.T) { + PackageVer = "" + PackageRev = "" + + result := GetBinaryVersion() + assert.Equal(t, "develop", result) +} + +func TestGetBinaryVersion_WithVersion(t *testing.T) { + PackageVer = "1.2.0" + PackageRev = "" + defer func() { PackageVer = "" }() + + result := GetBinaryVersion() + assert.Equal(t, "1.2.0", result) +} + +func TestGetBinaryVersion_WithVersionAndRevision(t *testing.T) { + PackageVer = "1.2.0" + PackageRev = "abc1234" + defer func() { + PackageVer = "" + PackageRev = "" + }() + + result := GetBinaryVersion() + assert.Equal(t, "1.2.0-abc1234", result) +} + +func TestGetBinaryVersion_WithRevisionOnly(t *testing.T) { + PackageVer = "" + PackageRev = "abc1234" + defer func() { PackageRev = "" }() + + result := GetBinaryVersion() + assert.Equal(t, "develop-abc1234", result) +} + +func TestIsDevelopMode_True(t *testing.T) { + PackageVer = "" + + assert.True(t, IsDevelopMode()) +} + +func TestIsDevelopMode_False(t *testing.T) { + PackageVer = "1.0.0" + defer func() { PackageVer = "" }() + + assert.False(t, IsDevelopMode()) +} + +func TestGetBinaryName_Default(t *testing.T) { + PackageName = "" + + result := GetBinaryName() + assert.Equal(t, "pentagi", result) +} + +func TestGetBinaryName_Custom(t *testing.T) { + PackageName = "myservice" + defer func() { PackageName = "" }() + + result := GetBinaryName() + assert.Equal(t, "myservice", result) +} diff --git a/docker-compose.yml b/docker-compose.yml index 8fcf8877..2b6adb70 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,6 +111,7 @@ services: - PROXY_URL=${PROXY_URL:-} - EXTERNAL_SSL_CA_PATH=${EXTERNAL_SSL_CA_PATH:-} - EXTERNAL_SSL_INSECURE=${EXTERNAL_SSL_INSECURE:-} + - HTTP_CLIENT_TIMEOUT=${HTTP_CLIENT_TIMEOUT:-} - SCRAPER_PUBLIC_URL=${SCRAPER_PUBLIC_URL:-} - SCRAPER_PRIVATE_URL=${SCRAPER_PRIVATE_URL:-} - GRAPHITI_ENABLED=${GRAPHITI_ENABLED:-} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 614dc293..95def383 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -72,10 +72,10 @@ "@eslint/eslintrc": "^3.3.1", "@graphql-codegen/cli": "^5.0.3", "@graphql-codegen/client-preset": "^4.5.1", - "@graphql-codegen/near-operation-file-preset": "^3.0.0", + "@graphql-codegen/near-operation-file-preset": "^5.0.0", "@graphql-codegen/typescript": "^4.1.1", "@graphql-codegen/typescript-operations": "^4.3.1", - "@graphql-codegen/typescript-react-apollo": "^4.3.2", + "@graphql-codegen/typescript-react-apollo": "^4.4.1", "@prettier/plugin-xml": "^3.3.1", "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.15", @@ -164,47 +164,20 @@ } }, "node_modules/@ardatan/relay-compiler": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.3.tgz", - "integrity": "sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-13.0.0.tgz", + "integrity": "sha512-ite4+xng5McO8MflWCi0un0YmnorTujsDnfPfhzYzAgoJ+jkI1pZj6jtmTl8Jptyi1H+Pa0zlatJIsxDD++ETA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", "@babel/runtime": "^7.26.10", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" + "immutable": "^5.1.5", + "invariant": "^2.2.4" }, "peerDependencies": { "graphql": "*" } }, - "node_modules/@ardatan/relay-compiler/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -287,19 +260,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", @@ -337,38 +297,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -378,20 +306,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -423,19 +337,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", @@ -446,38 +347,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -535,16 +404,14 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -553,593 +420,160 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", - "dev": true, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", - "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", - "dev": true, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "node_modules/@commitlint/cli": { + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.1.0.tgz", + "integrity": "sha512-pW5ujjrOovhq5RcYv5xCpb4GkZxkO2+GtOdBW2/qrr0Ll9tl3PX0aBBobGQl3mdZUbOBgwAexEQLeH6uxL0VYg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@commitlint/format": "^20.0.0", + "@commitlint/lint": "^20.0.0", + "@commitlint/load": "^20.1.0", + "@commitlint/read": "^20.0.0", + "@commitlint/types": "^20.0.0", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "node_modules/@commitlint/config-conventional": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.0.0.tgz", + "integrity": "sha512-q7JroPIkDBtyOkVe9Bca0p7kAUYxZMxkrBArCfuD3yN4KjRAenP9PmYwnn7rsw8Q+hHq1QB2BRmBh0/Z19ZoJw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@commitlint/types": "^20.0.0", + "conventional-changelog-conventionalcommits": "^7.0.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=v18" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "node_modules/@commitlint/config-validator": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.0.0.tgz", + "integrity": "sha512-BeyLMaRIJDdroJuYM2EGhDMGwVBMZna9UiIqV9hxj+J551Ctc6yoGuGSmghOy/qPhBSuhA6oMtbEiTmxECafsg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@commitlint/types": "^20.0.0", + "ajv": "^8.11.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=v18" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "node_modules/@commitlint/ensure": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.0.0.tgz", + "integrity": "sha512-WBV47Fffvabe68n+13HJNFBqiMH5U1Ryls4W3ieGwPC0C7kJqp3OVQQzG2GXqOALmzrgAB+7GXmyy8N9ct8/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@commitlint/types": "^20.0.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=v18" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=v18" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "node_modules/@commitlint/format": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.0.0.tgz", + "integrity": "sha512-zrZQXUcSDmQ4eGGrd+gFESiX0Rw+WFJk7nW4VFOmxub4mAATNKBQ4vNw5FgMCVehLUKG2OT2LjOqD0Hk8HvcRg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@commitlint/types": "^20.0.0", + "chalk": "^5.3.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-flow": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@commitlint/cli": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.1.0.tgz", - "integrity": "sha512-pW5ujjrOovhq5RcYv5xCpb4GkZxkO2+GtOdBW2/qrr0Ll9tl3PX0aBBobGQl3mdZUbOBgwAexEQLeH6uxL0VYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/format": "^20.0.0", - "@commitlint/lint": "^20.0.0", - "@commitlint/load": "^20.1.0", - "@commitlint/read": "^20.0.0", - "@commitlint/types": "^20.0.0", - "tinyexec": "^1.0.0", - "yargs": "^17.0.0" - }, - "bin": { - "commitlint": "cli.js" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-conventional": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.0.0.tgz", - "integrity": "sha512-q7JroPIkDBtyOkVe9Bca0p7kAUYxZMxkrBArCfuD3yN4KjRAenP9PmYwnn7rsw8Q+hHq1QB2BRmBh0/Z19ZoJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.0.0", - "conventional-changelog-conventionalcommits": "^7.0.2" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-validator": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.0.0.tgz", - "integrity": "sha512-BeyLMaRIJDdroJuYM2EGhDMGwVBMZna9UiIqV9hxj+J551Ctc6yoGuGSmghOy/qPhBSuhA6oMtbEiTmxECafsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.0.0", - "ajv": "^8.11.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/ensure": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.0.0.tgz", - "integrity": "sha512-WBV47Fffvabe68n+13HJNFBqiMH5U1Ryls4W3ieGwPC0C7kJqp3OVQQzG2GXqOALmzrgAB+7GXmyy8N9ct8/Fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.0.0", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/execute-rule": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", - "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/format": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.0.0.tgz", - "integrity": "sha512-zrZQXUcSDmQ4eGGrd+gFESiX0Rw+WFJk7nW4VFOmxub4mAATNKBQ4vNw5FgMCVehLUKG2OT2LjOqD0Hk8HvcRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.0.0", - "chalk": "^5.3.0" - }, - "engines": { - "node": ">=v18" + "node": ">=v18" } }, "node_modules/@commitlint/is-ignored": { @@ -2343,383 +1777,158 @@ "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^5.1.0", - "@graphql-codegen/visitor-plugin-common": "5.8.0", - "@graphql-tools/utils": "^10.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.6.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/gql-tag-operations/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/near-operation-file-preset": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/near-operation-file-preset/-/near-operation-file-preset-3.1.0.tgz", - "integrity": "sha512-sXIIi0BPP3IcARdzSztpE51oJTcGB67hi7ddBYfLinks/R/5aFG1Ry/J61707Kt+6Q5WhTnf5XAQUAqi6200yA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/add": "^3.2.1", - "@graphql-codegen/plugin-helpers": "^3.0.0", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "@graphql-tools/utils": "^10.0.0", - "parse-filepath": "^1.0.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@ardatan/relay-compiler": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", - "integrity": "sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/runtime": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "fbjs": "^3.0.0", - "glob": "^7.1.1", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" - }, - "peerDependencies": { - "graphql": "*" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/add": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-3.2.3.tgz", - "integrity": "sha512-sQOnWpMko4JLeykwyjFTxnhqjd/3NOG2OyMuvK76Wnnwh8DRrNf2VEs2kmSvLl7MndMlOj7Kh5U154dVcvhmKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.1", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/add/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/plugin-helpers": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", - "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^9.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/plugin-helpers/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "2.13.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.8.tgz", - "integrity": "sha512-IQWu99YV4wt8hGxIbBQPtqRuaWZhkQRG2IZKbMoSvh0vGeWb3dB0n0hSgKaOOxDY+tljtOf9MTcUYvJslQucMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-tools/optimize": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.4.0.tgz", - "integrity": "sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "6.5.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz", - "integrity": "sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ardatan/relay-compiler": "12.0.0", - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-tools/relay-operation-optimizer/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@graphql-codegen/plugin-helpers": "^5.1.0", + "@graphql-codegen/visitor-plugin-common": "5.8.0", + "@graphql-tools/utils": "^10.0.0", + "auto-bind": "~4.0.0", + "tslib": "~2.6.0" }, "engines": { - "node": "*" + "node": ">=16" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/locate-path": { + "node_modules/@graphql-codegen/gql-tag-operations/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-codegen/near-operation-file-preset": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "resolved": "https://registry.npmjs.org/@graphql-codegen/near-operation-file-preset/-/near-operation-file-preset-5.0.0.tgz", + "integrity": "sha512-Ck5jMu2izbKFwODYCM/sGWNQKN4BN4fpxr8g99fs6ESH29yfWMjA1dbUlTbtQtR7iXy39jT8451BLTG4/vOIxQ==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "@graphql-codegen/add": "^6.0.0", + "@graphql-codegen/plugin-helpers": "^6.1.1", + "@graphql-codegen/visitor-plugin-common": "^6.2.4", + "@graphql-tools/utils": "^11.0.0", + "parse-filepath": "^1.0.2", + "tslib": "^2.8.1" }, "engines": { - "node": ">=8" + "node": ">= 16.0.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/add": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-6.0.0.tgz", + "integrity": "sha512-biFdaURX0KTwEJPQ1wkT6BRgNasqgQ5KbCI1a3zwtLtO7XTo7/vKITPylmiU27K5DSOWYnY/1jfSqUAEBuhZrQ==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@graphql-codegen/plugin-helpers": "^6.0.0", + "tslib": "~2.6.0" }, "engines": { - "node": ">=6" + "node": ">=16" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/add/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/plugin-helpers": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.2.0.tgz", + "integrity": "sha512-TKm0Q0+wRlg354Qt3PyXc+sy6dCKxmNofBsgmHoFZNVHtzMQSSgNT+rUWdwBwObQ9bFHiUVsDIv8QqxKMiKmpw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@graphql-tools/utils": "^11.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.6.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.2.4.tgz", + "integrity": "sha512-iwiVCc7Mv8/XAa3K35AdFQ9chJSDv/gYEnBeQFF/Sq/W8EyJoHypOGOTTLk7OSrWO4xea65ggv0e7fGt7rPJjQ==", "dev": true, "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.1", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.1.1", + "@graphql-tools/utils": "^11.0.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "dependency-graph": "^1.0.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.6.0" + }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true, - "license": "ISC" + "license": "0BSD" }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "node_modules/@graphql-codegen/near-operation-file-preset/node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/@graphql-codegen/plugin-helpers": { @@ -2840,325 +2049,125 @@ "graphql-sock": "^1.0.0" }, "peerDependenciesMeta": { - "graphql-sock": { - "optional": true - } - } - }, - "node_modules/@graphql-codegen/typescript-operations/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/typescript-react-apollo": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-4.3.3.tgz", - "integrity": "sha512-ecuzzqoZEHCtlxaEXL1LQTrfzVYwNNtbVUBHc/KQDfkJIQZon+dG5ZXOoJ4BpbRA2L99yTx+TZc2VkpOVfSypw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.0.0", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@ardatan/relay-compiler": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", - "integrity": "sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/runtime": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "fbjs": "^3.0.0", - "glob": "^7.1.1", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" - }, - "peerDependencies": { - "graphql": "*" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/plugin-helpers": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", - "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^9.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "2.13.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.8.tgz", - "integrity": "sha512-IQWu99YV4wt8hGxIbBQPtqRuaWZhkQRG2IZKbMoSvh0vGeWb3dB0n0hSgKaOOxDY+tljtOf9MTcUYvJslQucMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-tools/optimize": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.4.0.tgz", - "integrity": "sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "6.5.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz", - "integrity": "sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ardatan/relay-compiler": "12.0.0", - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "graphql-sock": { + "optional": true + } } }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@graphql-codegen/typescript-operations/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } + "license": "0BSD" }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@graphql-codegen/typescript-react-apollo": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-4.4.1.tgz", + "integrity": "sha512-lrjUfDCNlCWQU07jO6EvZE8I2OLfJl9XKKGCcol27OhW6B9xaUEPaId+TvL6o/NfV+T4z4eQ/Y8BuKWyYjD+mQ==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@graphql-codegen/plugin-helpers": "^6.1.1", + "@graphql-codegen/visitor-plugin-common": "^6.2.4", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "tslib": "^2.8.1" }, "engines": { - "node": ">=6" + "node": ">= 16.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/plugin-helpers": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.2.0.tgz", + "integrity": "sha512-TKm0Q0+wRlg354Qt3PyXc+sy6dCKxmNofBsgmHoFZNVHtzMQSSgNT+rUWdwBwObQ9bFHiUVsDIv8QqxKMiKmpw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@graphql-tools/utils": "^11.0.0", + "change-case-all": "1.0.15", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "lodash": "~4.17.0", + "tslib": "~2.6.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.2.4.tgz", + "integrity": "sha512-iwiVCc7Mv8/XAa3K35AdFQ9chJSDv/gYEnBeQFF/Sq/W8EyJoHypOGOTTLk7OSrWO4xea65ggv0e7fGt7rPJjQ==", "dev": true, "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^6.1.1", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.1.1", + "@graphql-tools/utils": "^11.0.0", + "auto-bind": "~4.0.0", + "change-case-all": "1.0.15", + "dependency-graph": "^1.0.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "~2.6.0" + }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true, - "license": "ISC" + "license": "0BSD" }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "node_modules/@graphql-codegen/typescript-react-apollo/node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/@graphql-codegen/typescript/node_modules/tslib": { @@ -3655,14 +2664,33 @@ } }, "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.23.tgz", - "integrity": "sha512-L1i7QkiEJHsx7quHZsF8RUZ//a3tK6OTcideguHouf8tYfL87hKSngiDldVbN2QBSSvaUG6YlXv+AKH2yRXq3w==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.1.1.tgz", + "integrity": "sha512-va+ZieMlz6Fj18xUbwyQkZ34PsnzIdPT6Ccy1BNOQw1iclQwk52HejLMZeE/4fH+4cu80Q2HXi5+FjCKpmnJCg==", "dev": true, "license": "MIT", "dependencies": { - "@ardatan/relay-compiler": "^12.0.3", - "@graphql-tools/utils": "^10.10.1", + "@ardatan/relay-compiler": "^13.0.0", + "@graphql-tools/utils": "^11.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/relay-operation-optimizer/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "engines": { @@ -7557,13 +6585,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -7733,52 +6754,6 @@ "node": ">= 6" } }, - "node_modules/babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -7946,16 +6921,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -8057,16 +7022,6 @@ "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001754", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", @@ -8913,16 +7868,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -9155,9 +8100,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -10134,39 +9079,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fbjs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", - "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^1.0.35" - } - }, - "node_modules/fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -10451,13 +9363,6 @@ "node": ">=12" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -11462,14 +10367,11 @@ } }, "node_modules/immutable": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", - "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.8.0" - } + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.1", @@ -11530,18 +10432,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -14600,13 +13490,6 @@ "he": "1.2.0" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -14636,13 +13519,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -14750,16 +13626,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -14942,16 +13808,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -15089,16 +13945,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -15359,16 +14205,6 @@ } } }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "asap": "~2.0.3" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -15844,18 +14680,6 @@ "node": ">= 0.10" } }, - "node_modules/relay-runtime": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", - "integrity": "sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "fbjs": "^3.0.0", - "invariant": "^2.2.4" - } - }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -15965,13 +14789,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true, - "license": "ISC" - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -16302,13 +15119,6 @@ "upper-case-first": "^2.0.2" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -16364,13 +15174,6 @@ "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16503,13 +15306,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/signedsource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", - "integrity": "sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/simple-git-hooks": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/simple-git-hooks/-/simple-git-hooks-2.13.1.tgz", @@ -17406,33 +16202,6 @@ "node": ">=14.17" } }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -18235,13 +17004,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true, - "license": "ISC" - }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -18306,13 +17068,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 935880d2..c86e4f2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -82,10 +82,10 @@ "@eslint/eslintrc": "^3.3.1", "@graphql-codegen/cli": "^5.0.3", "@graphql-codegen/client-preset": "^4.5.1", - "@graphql-codegen/near-operation-file-preset": "^3.0.0", + "@graphql-codegen/near-operation-file-preset": "^5.0.0", "@graphql-codegen/typescript": "^4.1.1", "@graphql-codegen/typescript-operations": "^4.3.1", - "@graphql-codegen/typescript-react-apollo": "^4.3.2", + "@graphql-codegen/typescript-react-apollo": "^4.4.1", "@prettier/plugin-xml": "^3.3.1", "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.15", diff --git a/frontend/src/lib/apollo.ts b/frontend/src/lib/apollo.ts index 2973d1db..d8020b08 100644 --- a/frontend/src/lib/apollo.ts +++ b/frontend/src/lib/apollo.ts @@ -19,7 +19,7 @@ const ASSISTANT_LOG_TYPENAME = 'AssistantLog'; const MAX_RETRY_DELAY_MS = 30_000; const STREAMING_CACHE_MAX_ENTRIES = 500; const STREAMING_CACHE_TTL_MS = 1000 * 60 * 5; -const STREAMING_THROTTLE_MS = 100; +const STREAMING_THROTTLE_MS = 50; // --- Types --- @@ -35,8 +35,17 @@ type SubscriptionAction = 'add' | 'create' | 'delete' | 'update'; const EMPTY_LOG_ENTRY: StreamingLogEntry = { message: null, result: null, thinking: null }; -const concatStrings = (existing: null | string | undefined, incoming: null | string | undefined): null | string => - existing && incoming ? `${existing}${incoming}` : (existing ?? incoming ?? null); +const concatStrings = (existing: null | string | undefined, incoming: null | string | undefined): null | string => { + if (existing === null || existing === undefined) { + return incoming ?? null; + } + + if (incoming === null || incoming === undefined) { + return existing; + } + + return `${existing}${incoming}`; +}; const resolveSubscriptionAction = (name: string): SubscriptionAction => { if (name.endsWith('Deleted')) {