Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Code
c299afa185 ci: bump setup-python v5 -> v6 in setup-backend to run on Node 24
GitHub forces Actions off Node 20 starting June 16, 2026. The shared
setup-backend composite action still pins actions/setup-python@v5 (Node 20),
which surfaces a deprecation warning on every backend job. Bump to v6 (Node 24),
reusing the same pinned SHA already used in bump-python-package.yml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 10:54:53 -07:00
25 changed files with 244 additions and 987 deletions

View File

@@ -42,7 +42,7 @@ runs:
fi fi
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT" echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }} - name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version: ${{ steps.set-python-version.outputs.python-version }} python-version: ${{ steps.set-python-version.outputs.python-version }}
cache: ${{ inputs.cache }} cache: ${{ inputs.cache }}

View File

@@ -121,20 +121,6 @@ This change is backward compatible. The feature is off by default, and even when
Disabling a user account (setting `active` to `False`, via the admin UI, REST API, or CLI) now terminates that user's outstanding sessions on their next request, instead of relying on a passive check. This works for both client-side cookie sessions and server-side session stores via a per-user invalidation epoch (`user_attribute.sessions_invalidated_at`, added by a migration). The mechanism is inert for users that were never disabled (NULL epoch), so there is no behavior change for active users. Re-enabling an account and logging in again starts a fresh, valid session. The migration backfills the epoch for accounts that are already disabled at upgrade time, so re-enabling such an account does not revive a session that predates this feature. Disabling a user account (setting `active` to `False`, via the admin UI, REST API, or CLI) now terminates that user's outstanding sessions on their next request, instead of relying on a passive check. This works for both client-side cookie sessions and server-side session stores via a per-user invalidation epoch (`user_attribute.sessions_invalidated_at`, added by a migration). The mechanism is inert for users that were never disabled (NULL epoch), so there is no behavior change for active users. Re-enabling an account and logging in again starts a fresh, valid session. The migration backfills the epoch for accounts that are already disabled at upgrade time, so re-enabling such an account does not revive a session that predates this feature.
### Opt-in SSH tunnel server host key verification
SSH tunnels can now optionally pin the expected SSH server host key as a defense-in-depth measure against man-in-the-middle attacks. paramiko's transport performs no known-hosts checking by default, so previously the SSH server's identity was not verified. This feature is opt-in and off by default; existing tunnels are unaffected.
- A new nullable `server_host_key` column on the `ssh_tunnels` table stores the expected host key in authorized-key form (e.g. `ssh-ed25519 AAAA...`). It is a public key and is stored in plaintext. It can be set via the SSH tunnel POST/PUT payloads (`ssh_tunnel.server_host_key`).
- When a tunnel has `server_host_key` set, Superset connects to the SSH server, reads the host key it presents, and rejects the tunnel if it does not match.
- A new config flag `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING` (default `False`) controls fail-closed behavior. When `True`, every tunnel must declare a `server_host_key`; a tunnel without one is rejected.
Runbook to adopt:
1. Capture the SSH server's host key, e.g. `ssh-keyscan -t ed25519 ssh.example.com` (verify it out-of-band).
2. Set that value on the tunnel's `server_host_key` (via the database/SSH tunnel API or UI payload).
3. Optionally set `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING = True` in `superset_config.py` to require host-key verification on all tunnels.
### Dataset import validates catalog against the target connection ### Dataset import validates catalog against the target connection
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database. Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.

View File

@@ -72,23 +72,20 @@ services:
- -c - -c
- | - |
url="http://host.docker.internal:9000/static/assets/manifest.json" url="http://host.docker.internal:9000/static/assets/manifest.json"
max_attempts=300 # ~10 minutes at 2s intervals; first build can be slow max_attempts=150 # ~5 minutes at 2s intervals
echo "Waiting for webpack dev server at $$url..." echo "Waiting for webpack dev server at $url..."
attempt=0 attempt=0
until curl -sf --max-time 5 -H "Host: localhost" -o /dev/null "$$url"; do until curl -sf --max-time 5 -o /dev/null "$url"; do
attempt=$$((attempt + 1)) attempt=$((attempt + 1))
if [ "$$attempt" -ge "$$max_attempts" ]; then if [ "$attempt" -ge "$max_attempts" ]; then
echo "ERROR: webpack dev server did not serve $$url after $$max_attempts attempts." >&2 echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2 echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
exit 1 exit 1
fi fi
if [ $$((attempt % 15)) -eq 0 ]; then
echo "Still waiting for webpack dev server... ($$attempt/$$max_attempts)"
fi
sleep 2 sleep 2
done done
echo "Webpack dev server is ready; starting nginx." echo "Webpack dev server is ready; starting nginx."
exec /docker-entrypoint.sh nginx -g 'daemon off;' exec nginx -g 'daemon off;'
redis: redis:
image: redis:7 image: redis:7

View File

@@ -81,17 +81,17 @@ case "${1}" in
app) app)
echo "Starting web app (using development server)..." echo "Starting web app (using development server)..."
# Always run in Flask debug mode here: this is the dev compose entrypoint, # Environment-based debugger control for security
# and Superset's Talisman selector keys off app.debug to serve the dev CSP # Only enable Werkzeug interactive debugger when explicitly requested
# (which permits 'unsafe-eval' required by React Refresh / HMR). # Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
export FLASK_DEBUG=1 # Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
# Werkzeug's interactive debugger (/console) is a separate, security-sensitive
# feature and must be opted into explicitly via SUPERSET_DEBUG_ENABLED=true.
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
export FLASK_DEBUG=1
DEBUGGER_FLAG="--debugger" DEBUGGER_FLAG="--debugger"
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)" echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
else else
export FLASK_DEBUG=0
DEBUGGER_FLAG="--no-debugger" DEBUGGER_FLAG="--no-debugger"
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)" echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi fi

View File

@@ -265,15 +265,6 @@
js-tokens "^4.0.0" js-tokens "^4.0.0"
picocolors "^1.1.1" picocolors "^1.1.1"
"@babel/code-frame@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7"
integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==
dependencies:
"@babel/helper-validator-identifier" "^7.29.7"
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0": "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0":
version "7.28.0" version "7.28.0"
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
@@ -284,25 +275,20 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz"
integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==
"@babel/compat-data@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629"
integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==
"@babel/core@^7.21.3", "@babel/core@^7.25.9": "@babel/core@^7.21.3", "@babel/core@^7.25.9":
version "7.29.7" version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz"
integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==
dependencies: dependencies:
"@babel/code-frame" "^7.29.7" "@babel/code-frame" "^7.28.6"
"@babel/generator" "^7.29.7" "@babel/generator" "^7.28.6"
"@babel/helper-compilation-targets" "^7.29.7" "@babel/helper-compilation-targets" "^7.28.6"
"@babel/helper-module-transforms" "^7.29.7" "@babel/helper-module-transforms" "^7.28.6"
"@babel/helpers" "^7.29.7" "@babel/helpers" "^7.28.6"
"@babel/parser" "^7.29.7" "@babel/parser" "^7.28.6"
"@babel/template" "^7.29.7" "@babel/template" "^7.28.6"
"@babel/traverse" "^7.29.7" "@babel/traverse" "^7.28.6"
"@babel/types" "^7.29.7" "@babel/types" "^7.28.6"
"@jridgewell/remapping" "^2.3.5" "@jridgewell/remapping" "^2.3.5"
convert-source-map "^2.0.0" convert-source-map "^2.0.0"
debug "^4.1.0" debug "^4.1.0"
@@ -332,17 +318,6 @@
"@jridgewell/trace-mapping" "^0.3.28" "@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2" jsesc "^3.0.2"
"@babel/generator@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3"
integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==
dependencies:
"@babel/parser" "^7.29.7"
"@babel/types" "^7.29.7"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3":
version "7.27.3" version "7.27.3"
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz"
@@ -350,7 +325,7 @@
dependencies: dependencies:
"@babel/types" "^7.27.3" "@babel/types" "^7.27.3"
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6":
version "7.28.6" version "7.28.6"
resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz" resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz"
integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==
@@ -361,17 +336,6 @@
lru-cache "^5.1.1" lru-cache "^5.1.1"
semver "^6.3.1" semver "^6.3.1"
"@babel/helper-compilation-targets@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042"
integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==
dependencies:
"@babel/compat-data" "^7.29.7"
"@babel/helper-validator-option" "^7.29.7"
browserslist "^4.24.0"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3": "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3":
version "7.28.3" version "7.28.3"
resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz" resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz"
@@ -410,11 +374,6 @@
resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz"
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
"@babel/helper-globals@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b"
integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==
"@babel/helper-member-expression-to-functions@^7.27.1": "@babel/helper-member-expression-to-functions@^7.27.1":
version "7.27.1" version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz"
@@ -439,14 +398,6 @@
"@babel/traverse" "^7.28.6" "@babel/traverse" "^7.28.6"
"@babel/types" "^7.28.6" "@babel/types" "^7.28.6"
"@babel/helper-module-imports@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396"
integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==
dependencies:
"@babel/traverse" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6": "@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6":
version "7.28.6" version "7.28.6"
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz"
@@ -456,15 +407,6 @@
"@babel/helper-validator-identifier" "^7.28.5" "@babel/helper-validator-identifier" "^7.28.5"
"@babel/traverse" "^7.28.6" "@babel/traverse" "^7.28.6"
"@babel/helper-module-transforms@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae"
integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==
dependencies:
"@babel/helper-module-imports" "^7.29.7"
"@babel/helper-validator-identifier" "^7.29.7"
"@babel/traverse" "^7.29.7"
"@babel/helper-optimise-call-expression@^7.27.1": "@babel/helper-optimise-call-expression@^7.27.1":
version "7.27.1" version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz"
@@ -513,31 +455,16 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-string-parser@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f"
integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==
"@babel/helper-validator-identifier@^7.28.5": "@babel/helper-validator-identifier@^7.28.5":
version "7.28.5" version "7.28.5"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@babel/helper-validator-identifier@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2"
integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==
"@babel/helper-validator-option@^7.27.1": "@babel/helper-validator-option@^7.27.1":
version "7.27.1" version "7.27.1"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz"
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
"@babel/helper-validator-option@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a"
integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==
"@babel/helper-wrap-function@^7.27.1": "@babel/helper-wrap-function@^7.27.1":
version "7.28.3" version "7.28.3"
resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz"
@@ -547,13 +474,13 @@
"@babel/traverse" "^7.28.3" "@babel/traverse" "^7.28.3"
"@babel/types" "^7.28.2" "@babel/types" "^7.28.2"
"@babel/helpers@^7.29.7": "@babel/helpers@^7.28.6":
version "7.29.7" version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz"
integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==
dependencies: dependencies:
"@babel/template" "^7.29.7" "@babel/template" "^7.28.6"
"@babel/types" "^7.29.7" "@babel/types" "^7.28.6"
"@babel/parser@^7.28.6": "@babel/parser@^7.28.6":
version "7.28.6" version "7.28.6"
@@ -569,13 +496,6 @@
dependencies: dependencies:
"@babel/types" "^7.29.0" "@babel/types" "^7.29.0"
"@babel/parser@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334"
integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==
dependencies:
"@babel/types" "^7.29.7"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
version "7.27.1" version "7.27.1"
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz"
@@ -1252,15 +1172,6 @@
"@babel/parser" "^7.28.6" "@babel/parser" "^7.28.6"
"@babel/types" "^7.28.6" "@babel/types" "^7.28.6"
"@babel/template@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==
dependencies:
"@babel/code-frame" "^7.29.7"
"@babel/parser" "^7.29.7"
"@babel/types" "^7.29.7"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.6": "@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.6":
version "7.28.6" version "7.28.6"
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz"
@@ -1287,19 +1198,6 @@
"@babel/types" "^7.29.0" "@babel/types" "^7.29.0"
debug "^4.3.1" debug "^4.3.1"
"@babel/traverse@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d"
integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==
dependencies:
"@babel/code-frame" "^7.29.7"
"@babel/generator" "^7.29.7"
"@babel/helper-globals" "^7.29.7"
"@babel/parser" "^7.29.7"
"@babel/template" "^7.29.7"
"@babel/types" "^7.29.7"
debug "^4.3.1"
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.4.4": "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.4.4":
version "7.28.6" version "7.28.6"
resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz" resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz"
@@ -1316,14 +1214,6 @@
"@babel/helper-string-parser" "^7.27.1" "@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5" "@babel/helper-validator-identifier" "^7.28.5"
"@babel/types@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
dependencies:
"@babel/helper-string-parser" "^7.29.7"
"@babel/helper-validator-identifier" "^7.29.7"
"@braintree/sanitize-url@^7.1.1": "@braintree/sanitize-url@^7.1.1":
version "7.1.2" version "7.1.2"
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz#ca2035b0fefe956a8676ff0c69af73e605fcd81f" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz#ca2035b0fefe956a8676ff0c69af73e605fcd81f"
@@ -9642,12 +9532,12 @@ latest-version@^7.0.0:
package-json "^8.1.0" package-json "^8.1.0"
launch-editor@^2.6.1: launch-editor@^2.6.1:
version "2.14.1" version "2.11.1"
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.14.1.tgz#f7e0da3f58aaea03fea01074d840b5f739ed7ddc" resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz"
integrity sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA== integrity sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==
dependencies: dependencies:
picocolors "^1.1.1" picocolors "^1.1.1"
shell-quote "^1.8.4" shell-quote "^1.8.3"
layout-base@^1.0.0: layout-base@^1.0.0:
version "1.0.2" version "1.0.2"
@@ -13599,7 +13489,7 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.8.4: shell-quote@^1.8.3:
version "1.8.4" version "1.8.4"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ== integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==

View File

@@ -27,6 +27,19 @@
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
} }
}, },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/cli": { "node_modules/@babel/cli": {
"version": "7.25.6", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz",
@@ -58,12 +71,12 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.7", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.29.7", "@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
}, },
@@ -72,30 +85,32 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.29.7", "version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.29.6", "version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.6.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
"integrity": "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@ampproject/remapping": "^2.2.0",
"@babel/generator": "^7.29.6", "@babel/code-frame": "^7.24.7",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/generator": "^7.25.0",
"@babel/helper-module-transforms": "^7.28.6", "@babel/helper-compilation-targets": "^7.25.2",
"@babel/helpers": "^7.29.2", "@babel/helper-module-transforms": "^7.25.2",
"@babel/parser": "^7.29.3", "@babel/helpers": "^7.25.0",
"@babel/template": "^7.28.6", "@babel/parser": "^7.25.0",
"@babel/traverse": "^7.29.0", "@babel/template": "^7.25.0",
"@babel/types": "^7.29.0", "@babel/traverse": "^7.25.2",
"@jridgewell/remapping": "^2.3.5", "@babel/types": "^7.25.2",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@@ -111,13 +126,13 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.29.7", "version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.7", "@babel/parser": "^7.29.0",
"@babel/types": "^7.29.7", "@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@@ -154,14 +169,15 @@
} }
}, },
"node_modules/@babel/helper-compilation-targets": { "node_modules/@babel/helper-compilation-targets": {
"version": "7.29.7", "version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.29.7", "@babel/compat-data": "^7.25.2",
"@babel/helper-validator-option": "^7.29.7", "@babel/helper-validator-option": "^7.24.8",
"browserslist": "^4.24.0", "browserslist": "^4.23.1",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"semver": "^6.3.1" "semver": "^6.3.1"
}, },
@@ -366,28 +382,29 @@
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.29.7", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-option": { "node_modules/@babel/helper-validator-option": {
"version": "7.29.7", "version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -408,25 +425,26 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.29.7", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.29.7", "@babel/template": "^7.25.0",
"@babel/types": "^7.29.7" "@babel/types": "^7.25.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.29.7", "version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.29.7" "@babel/types": "^7.29.0"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -1825,14 +1843,14 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.29.7", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.7", "@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.29.7", "@babel/parser": "^7.28.6",
"@babel/types": "^7.29.7" "@babel/types": "^7.28.6"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1857,13 +1875,13 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.7", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.29.7", "@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.29.7" "@babel/helper-validator-identifier": "^7.28.5"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -2631,16 +2649,6 @@
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
} }
}, },
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
@@ -7975,6 +7983,16 @@
} }
}, },
"dependencies": { "dependencies": {
"@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"@babel/cli": { "@babel/cli": {
"version": "7.25.6", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz",
@@ -7993,38 +8011,38 @@
} }
}, },
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.29.7", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.29.7", "@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0", "js-tokens": "^4.0.0",
"picocolors": "^1.1.1" "picocolors": "^1.1.1"
} }
}, },
"@babel/compat-data": { "@babel/compat-data": {
"version": "7.29.7", "version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
"dev": true "dev": true
}, },
"@babel/core": { "@babel/core": {
"version": "7.29.6", "version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.6.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
"integrity": "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.29.0", "@ampproject/remapping": "^2.2.0",
"@babel/generator": "^7.29.6", "@babel/code-frame": "^7.24.7",
"@babel/helper-compilation-targets": "^7.28.6", "@babel/generator": "^7.25.0",
"@babel/helper-module-transforms": "^7.28.6", "@babel/helper-compilation-targets": "^7.25.2",
"@babel/helpers": "^7.29.2", "@babel/helper-module-transforms": "^7.25.2",
"@babel/parser": "^7.29.3", "@babel/helpers": "^7.25.0",
"@babel/template": "^7.28.6", "@babel/parser": "^7.25.0",
"@babel/traverse": "^7.29.0", "@babel/template": "^7.25.0",
"@babel/types": "^7.29.0", "@babel/traverse": "^7.25.2",
"@jridgewell/remapping": "^2.3.5", "@babel/types": "^7.25.2",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@@ -8033,13 +8051,13 @@
} }
}, },
"@babel/generator": { "@babel/generator": {
"version": "7.29.7", "version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/parser": "^7.29.7", "@babel/parser": "^7.29.0",
"@babel/types": "^7.29.7", "@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@@ -8065,14 +8083,14 @@
} }
}, },
"@babel/helper-compilation-targets": { "@babel/helper-compilation-targets": {
"version": "7.29.7", "version": "7.25.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/compat-data": "^7.29.7", "@babel/compat-data": "^7.25.2",
"@babel/helper-validator-option": "^7.29.7", "@babel/helper-validator-option": "^7.24.8",
"browserslist": "^4.24.0", "browserslist": "^4.23.1",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"semver": "^6.3.1" "semver": "^6.3.1"
} }
@@ -8211,21 +8229,21 @@
} }
}, },
"@babel/helper-string-parser": { "@babel/helper-string-parser": {
"version": "7.29.7", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true "dev": true
}, },
"@babel/helper-validator-identifier": { "@babel/helper-validator-identifier": {
"version": "7.29.7", "version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true "dev": true
}, },
"@babel/helper-validator-option": { "@babel/helper-validator-option": {
"version": "7.29.7", "version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
"dev": true "dev": true
}, },
"@babel/helper-wrap-function": { "@babel/helper-wrap-function": {
@@ -8240,22 +8258,22 @@
} }
}, },
"@babel/helpers": { "@babel/helpers": {
"version": "7.29.7", "version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/template": "^7.29.7", "@babel/template": "^7.25.0",
"@babel/types": "^7.29.7" "@babel/types": "^7.25.6"
} }
}, },
"@babel/parser": { "@babel/parser": {
"version": "7.29.7", "version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/types": "^7.29.7" "@babel/types": "^7.29.0"
} }
}, },
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
@@ -9139,14 +9157,14 @@
} }
}, },
"@babel/template": { "@babel/template": {
"version": "7.29.7", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.29.7", "@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.29.7", "@babel/parser": "^7.28.6",
"@babel/types": "^7.29.7" "@babel/types": "^7.28.6"
} }
}, },
"@babel/traverse": { "@babel/traverse": {
@@ -9165,13 +9183,13 @@
} }
}, },
"@babel/types": { "@babel/types": {
"version": "7.29.7", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-string-parser": "^7.29.7", "@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.29.7" "@babel/helper-validator-identifier": "^7.28.5"
} }
}, },
"@bcoe/v8-coverage": { "@bcoe/v8-coverage": {
@@ -9753,16 +9771,6 @@
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
} }
}, },
"@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"requires": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"@jridgewell/resolve-uri": { "@jridgewell/resolve-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",

View File

@@ -4708,15 +4708,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.6", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4", "hasown": "^2.0.2",
"mime-types": "^2.1.35" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -5028,9 +5029,10 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.4", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@@ -10226,7 +10228,7 @@
"camelcase": "^5.3.1", "camelcase": "^5.3.1",
"find-up": "^4.1.0", "find-up": "^4.1.0",
"get-package-type": "^0.1.0", "get-package-type": "^0.1.0",
"js-yaml": "^3.13.1", "js-yaml": "4.1.1",
"resolve-from": "^5.0.0" "resolve-from": "^5.0.0"
}, },
"dependencies": { "dependencies": {
@@ -10236,7 +10238,8 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}, },
"js-yaml": { "js-yaml": {
"version": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"requires": { "requires": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -12342,15 +12345,15 @@
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
}, },
"form-data": { "form-data": {
"version": "4.0.6", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"requires": { "requires": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4", "hasown": "^2.0.2",
"mime-types": "^2.1.35" "mime-types": "^2.1.12"
} }
}, },
"fromentries": { "fromentries": {
@@ -12571,9 +12574,9 @@
} }
}, },
"hasown": { "hasown": {
"version": "2.0.4", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": { "requires": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
} }

View File

@@ -11349,6 +11349,19 @@
"integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/esrecurse": { "node_modules/@types/esrecurse": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -20669,35 +20682,22 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.6", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4", "hasown": "^2.0.2",
"mime-types": "^2.1.35" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/form-data/node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/format": { "node_modules/format": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
@@ -26524,14 +26524,14 @@
} }
}, },
"node_modules/launch-editor": { "node_modules/launch-editor": {
"version": "2.14.1", "version": "2.9.1",
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz",
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"picocolors": "^1.1.1", "picocolors": "^1.0.0",
"shell-quote": "^1.8.4" "shell-quote": "^1.8.1"
} }
}, },
"node_modules/lerc": { "node_modules/lerc": {

View File

@@ -33,7 +33,6 @@ from superset.commands.database.exceptions import (
from superset.commands.database.ssh_tunnel.exceptions import ( from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelCreateFailedError, SSHTunnelCreateFailedError,
SSHTunnelDatabasePortError, SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
SSHTunnelingNotEnabledError, SSHTunnelingNotEnabledError,
SSHTunnelInvalidError, SSHTunnelInvalidError,
) )
@@ -76,7 +75,6 @@ class CreateDatabaseCommand(BaseCommand):
SupersetErrorsException, SupersetErrorsException,
SSHTunnelingNotEnabledError, SSHTunnelingNotEnabledError,
SSHTunnelDatabasePortError, SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
) as ex: ) as ex:
event_logger.log_with_context( event_logger.log_with_context(
action=f"db_creation_failed.{ex.__class__.__name__}", action=f"db_creation_failed.{ex.__class__.__name__}",

View File

@@ -75,11 +75,3 @@ class SSHTunnelMissingCredentials(CommandInvalidError, SSHTunnelError): # noqa:
class SSHTunnelInvalidCredentials(CommandInvalidError, SSHTunnelError): # noqa: N818 class SSHTunnelInvalidCredentials(CommandInvalidError, SSHTunnelError): # noqa: N818
message = _("Cannot have multiple credentials for the SSH Tunnel") message = _("Cannot have multiple credentials for the SSH Tunnel")
class SSHTunnelHostKeyVerificationError(CommandInvalidError, SSHTunnelError):
"""The SSH server's host key failed opt-in verification for a tunnel."""
message = _(
"The SSH server host key could not be verified against the expected key."
)

View File

@@ -29,7 +29,6 @@ from superset.commands.database.exceptions import (
) )
from superset.commands.database.ssh_tunnel.exceptions import ( from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelDatabasePortError, SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
SSHTunnelingNotEnabledError, SSHTunnelingNotEnabledError,
) )
from superset.commands.database.utils import ping from superset.commands.database.utils import ping
@@ -222,11 +221,7 @@ class TestConnectionDatabaseCommand(BaseCommand):
engine=engine_name, engine=engine_name,
) )
raise DatabaseSecurityUnsafeError(message=str(ex)) from ex raise DatabaseSecurityUnsafeError(message=str(ex)) from ex
except ( except (SupersetTimeoutException, SSHTunnelingNotEnabledError) as ex:
SupersetTimeoutException,
SSHTunnelingNotEnabledError,
SSHTunnelHostKeyVerificationError,
) as ex:
event_logger.log_with_context( event_logger.log_with_context(
action=get_log_connection_action( action=get_log_connection_action(
"test_connection_error", "test_connection_error",
@@ -235,8 +230,7 @@ class TestConnectionDatabaseCommand(BaseCommand):
), ),
engine=engine_name, engine=engine_name,
) )
# bubble up the exception (preserving its specific message and status) # bubble up the exception to return proper status code
# instead of flattening it into a generic connection failure
raise raise
except Exception as ex: except Exception as ex:
if not database: if not database:

View File

@@ -895,15 +895,6 @@ SSH_TUNNEL_TIMEOUT_SEC = 10.0
#: Timeout (seconds) for transport socket (``socket.settimeout``) #: Timeout (seconds) for transport socket (``socket.settimeout``)
SSH_TUNNEL_PACKET_TIMEOUT_SEC = 1.0 SSH_TUNNEL_PACKET_TIMEOUT_SEC = 1.0
#: Opt-in defense-in-depth: when enabled, every SSH tunnel must declare an expected
#: server host key (``server_host_key`` on the tunnel) and the SSH server's presented
#: host key is verified against it before the tunnel is opened. A mismatch, or a
#: missing expected key while this flag is enabled, fails closed and the tunnel is
#: rejected. When disabled (the default), tunnels without a ``server_host_key`` open
#: without host-key verification, preserving existing behavior; tunnels that do set a
#: ``server_host_key`` are still verified regardless of this flag.
SSH_TUNNEL_STRICT_HOST_KEY_CHECKING: bool = False
# Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars. # Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars.
DEFAULT_FEATURE_FLAGS.update( DEFAULT_FEATURE_FLAGS.update(

View File

@@ -56,7 +56,6 @@ from superset.commands.database.importers.dispatcher import ImportDatabasesComma
from superset.commands.database.oauth2 import OAuth2StoreTokenCommand from superset.commands.database.oauth2 import OAuth2StoreTokenCommand
from superset.commands.database.ssh_tunnel.exceptions import ( from superset.commands.database.ssh_tunnel.exceptions import (
SSHTunnelDatabasePortError, SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
SSHTunnelingNotEnabledError, SSHTunnelingNotEnabledError,
) )
from superset.commands.database.sync_permissions import SyncPermissionsCommand from superset.commands.database.sync_permissions import SyncPermissionsCommand
@@ -485,11 +484,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
exc_info=True, exc_info=True,
) )
return self.response_422(message=str(ex)) return self.response_422(message=str(ex))
except ( except (SSHTunnelingNotEnabledError, SSHTunnelDatabasePortError) as ex:
SSHTunnelingNotEnabledError,
SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
) as ex:
return self.response_400(message=str(ex)) return self.response_400(message=str(ex))
except SupersetException as ex: except SupersetException as ex:
return self.response(ex.status, message=ex.message) return self.response(ex.status, message=ex.message)
@@ -574,11 +569,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
exc_info=True, exc_info=True,
) )
return self.response_422(message=str(ex)) return self.response_422(message=str(ex))
except ( except (SSHTunnelingNotEnabledError, SSHTunnelDatabasePortError) as ex:
SSHTunnelingNotEnabledError,
SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
) as ex:
return self.response_400(message=str(ex)) return self.response_400(message=str(ex))
@expose("/<int:pk>", methods=("DELETE",)) @expose("/<int:pk>", methods=("DELETE",))
@@ -1300,11 +1291,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
try: try:
TestConnectionDatabaseCommand(item).run() TestConnectionDatabaseCommand(item).run()
return self.response(200, message="OK") return self.response(200, message="OK")
except ( except (SSHTunnelingNotEnabledError, SSHTunnelDatabasePortError) as ex:
SSHTunnelingNotEnabledError,
SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
) as ex:
return self.response_400(message=str(ex)) return self.response_400(message=str(ex))
@expose("/<int:pk>/related_objects/", methods=("GET",)) @expose("/<int:pk>/related_objects/", methods=("GET",))

View File

@@ -477,22 +477,6 @@ class DatabaseSSHTunnel(Schema):
private_key = fields.String(required=False, load_only=True) private_key = fields.String(required=False, load_only=True)
private_key_password = fields.String(required=False, load_only=True) private_key_password = fields.String(required=False, load_only=True)
# Optional expected SSH server host key in authorized-key form
# (e.g. "ssh-rsa AAAA...", "ssh-ed25519 AAAA..."). When set, the SSH server's
# presented host key is verified against it before the tunnel is opened. This is
# a public key, so it is not sensitive and is not masked.
server_host_key = fields.String(
required=False,
allow_none=True,
metadata={
"description": (
"Expected SSH server host key in authorized-key form "
"(e.g. 'ssh-ed25519 AAAA...'). When set, the server's host key is "
"verified against it before the tunnel is opened."
)
},
)
@validates_schema @validates_schema
def validate_authentication(self, data: dict[str, Any], **kwargs: Any) -> None: def validate_authentication(self, data: dict[str, Any], **kwargs: Any) -> None:
errors: dict[str, str] = {} errors: dict[str, str] = {}

View File

@@ -72,12 +72,6 @@ class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model):
encrypted_field_factory.create(Text), nullable=True encrypted_field_factory.create(Text), nullable=True
) )
# Optional expected SSH server host key, in authorized-key form
# (e.g. "ssh-rsa AAAA...", "ssh-ed25519 AAAA..."). When set, the SSH server's
# presented host key is verified against this value before the tunnel is opened.
# This is a public key, so it is stored in plaintext (not encrypted).
server_host_key = sa.Column(sa.Text, nullable=True)
export_fields = [ export_fields = [
"server_address", "server_address",
"server_port", "server_port",
@@ -85,7 +79,6 @@ class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model):
"password", "password",
"private_key", "private_key",
"private_key_password", "private_key_password",
"server_host_key",
] ]
extra_import_fields = [ extra_import_fields = [
@@ -100,9 +93,6 @@ class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model):
"server_port": self.server_port, "server_port": self.server_port,
"username": self.username, "username": self.username,
} }
if self.server_host_key is not None:
# public key, not sensitive: returned in cleartext
output["server_host_key"] = self.server_host_key
if self.password is not None: if self.password is not None:
output["password"] = PASSWORD_MASK output["password"] = PASSWORD_MASK
if self.private_key is not None: if self.private_key is not None:

View File

@@ -15,63 +15,26 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
import base64
import binascii
import logging import logging
import socket
from io import StringIO from io import StringIO
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import paramiko
import sshtunnel import sshtunnel
from flask import Flask from flask import Flask
from paramiko import RSAKey from paramiko import RSAKey
from paramiko.pkey import UnknownKeyType
from superset.commands.database.ssh_tunnel.exceptions import ( from superset.commands.database.ssh_tunnel.exceptions import SSHTunnelDatabasePortError
SSHTunnelDatabasePortError,
SSHTunnelHostKeyVerificationError,
)
from superset.databases.utils import make_url_safe from superset.databases.utils import make_url_safe
from superset.utils.class_utils import load_class_from_name from superset.utils.class_utils import load_class_from_name
if TYPE_CHECKING: if TYPE_CHECKING:
from superset.databases.ssh_tunnel.models import SSHTunnel from superset.databases.ssh_tunnel.models import SSHTunnel
logger = logging.getLogger(__name__)
def _parse_authorized_key(authorized_key: str) -> paramiko.PKey:
"""
Parse a host key in authorized-key form (``"<type> <base64>[ comment]"``) into a
:class:`paramiko.PKey`. The optional trailing comment field and surrounding
whitespace are ignored.
:raises ValueError: if the value is empty or cannot be parsed as a host key.
"""
fields = authorized_key.strip().split()
if len(fields) < 2:
raise ValueError("Host key must be in 'ssh-<type> <base64>' form")
key_type, key_b64 = fields[0], fields[1]
try:
# validate=True so malformed characters raise instead of being silently
# dropped, which could otherwise pin an unintended key value.
key_bytes = base64.b64decode(key_b64, validate=True)
except (binascii.Error, ValueError) as ex:
raise ValueError("Host key base64 payload could not be decoded") from ex
try:
return paramiko.PKey.from_type_string(key_type, key_bytes)
except (paramiko.SSHException, UnknownKeyType) as ex:
raise ValueError(f"Host key could not be parsed: {ex}") from ex
class SSHManager: class SSHManager:
def __init__(self, app: Flask) -> None: def __init__(self, app: Flask) -> None:
super().__init__() super().__init__()
self.local_bind_address = app.config["SSH_TUNNEL_LOCAL_BIND_ADDRESS"] self.local_bind_address = app.config["SSH_TUNNEL_LOCAL_BIND_ADDRESS"]
self.strict_host_key_checking = app.config.get(
"SSH_TUNNEL_STRICT_HOST_KEY_CHECKING", False
)
sshtunnel.TUNNEL_TIMEOUT = app.config["SSH_TUNNEL_TIMEOUT_SEC"] sshtunnel.TUNNEL_TIMEOUT = app.config["SSH_TUNNEL_TIMEOUT_SEC"]
sshtunnel.SSH_TIMEOUT = app.config["SSH_TUNNEL_PACKET_TIMEOUT_SEC"] sshtunnel.SSH_TIMEOUT = app.config["SSH_TUNNEL_PACKET_TIMEOUT_SEC"]
@@ -85,87 +48,6 @@ class SSHManager:
port=server.local_bind_port, port=server.local_bind_port,
) )
def _verify_host_key(self, ssh_tunnel: "SSHTunnel") -> "paramiko.PKey | None":
"""
Opt-in defense-in-depth: verify the SSH server's host key before opening the
tunnel, to resist man-in-the-middle attacks (paramiko's ``Transport`` does no
known-hosts checking by default).
Behavior:
- If the tunnel declares an expected ``server_host_key``, connect to the SSH
server, read the host key it presents, and compare. On mismatch (or if the
expected key cannot be parsed) raise
:class:`SSHTunnelHostKeyVerificationError`.
- If no expected key is set and ``SSH_TUNNEL_STRICT_HOST_KEY_CHECKING`` is
enabled, fail closed and raise.
- If no expected key is set and strict checking is disabled, do nothing,
preserving existing (unverified) behavior.
:returns: the parsed expected host key when one is configured (so the caller
can pin it on the tunnel's own connection), or ``None`` when no key is
configured.
"""
expected_raw = ssh_tunnel.server_host_key
if not expected_raw or not expected_raw.strip():
if self.strict_host_key_checking:
raise SSHTunnelHostKeyVerificationError(
message=(
"SSH_TUNNEL_STRICT_HOST_KEY_CHECKING is enabled but no "
"expected server host key is configured for this tunnel."
)
)
return None
try:
expected_key = _parse_authorized_key(expected_raw)
except ValueError as ex:
raise SSHTunnelHostKeyVerificationError(
message=f"The configured expected server host key is invalid: {ex}"
) from ex
# Build the socket ourselves with an explicit timeout so the TCP connect
# phase is bounded too. ``paramiko.Transport((host, port))`` would connect
# synchronously with no timeout, leaving ``start_client(timeout=...)`` to
# govern only the SSH handshake; an unreachable host could then block for the
# full OS-level TCP timeout.
try:
sock = socket.create_connection(
(ssh_tunnel.server_address, ssh_tunnel.server_port),
timeout=sshtunnel.SSH_TIMEOUT,
)
except OSError as ex:
raise SSHTunnelHostKeyVerificationError(
message=f"Could not connect to the SSH server: {ex}"
) from ex
transport = paramiko.Transport(sock)
try:
transport.start_client(timeout=sshtunnel.SSH_TIMEOUT)
remote_key = transport.get_remote_server_key()
except Exception as ex: # noqa: BLE001
raise SSHTunnelHostKeyVerificationError(
message=f"Could not retrieve the SSH server host key: {ex}"
) from ex
finally:
transport.close()
if remote_key != expected_key:
logger.warning(
"SSH host key mismatch for %s:%s",
ssh_tunnel.server_address,
ssh_tunnel.server_port,
)
raise SSHTunnelHostKeyVerificationError(
message=(
"The SSH server presented a host key that does not match the "
"expected server host key configured for this tunnel."
)
)
return expected_key
def create_tunnel( def create_tunnel(
self, self,
ssh_tunnel: "SSHTunnel", ssh_tunnel: "SSHTunnel",
@@ -178,12 +60,6 @@ class SSHManager:
port = url.port or get_default_port(backend) port = url.port or get_default_port(backend)
if not port: if not port:
raise SSHTunnelDatabasePortError() raise SSHTunnelDatabasePortError()
# Opt-in host-key verification runs before the tunnel is opened. It returns
# the parsed expected key (or None) so we can also pin it on the tunnel's own
# connection below.
expected_host_key = self._verify_host_key(ssh_tunnel)
params = { params = {
"ssh_address_or_host": (ssh_tunnel.server_address, ssh_tunnel.server_port), "ssh_address_or_host": (ssh_tunnel.server_address, ssh_tunnel.server_port),
"ssh_username": ssh_tunnel.username, "ssh_username": ssh_tunnel.username,
@@ -192,14 +68,6 @@ class SSHManager:
"debug_level": logging.getLogger("flask_appbuilder").level, "debug_level": logging.getLogger("flask_appbuilder").level,
} }
if expected_host_key is not None:
# Pin the expected key on the tunnel's own connection, so paramiko verifies
# the host that actually carries traffic on the same transport. The probe
# above and the tunnel open separate connections, so verifying only the
# probe would leave a TOCTOU gap (DNS re-resolution, selective
# interception); pinning here closes it.
params["ssh_host_key"] = expected_host_key
if ssh_tunnel.password: if ssh_tunnel.password:
params["ssh_password"] = ssh_tunnel.password params["ssh_password"] = ssh_tunnel.password
elif ssh_tunnel.private_key: elif ssh_tunnel.private_key:

View File

@@ -327,16 +327,7 @@ Chart Types You Can CREATE with generate_chart/generate_explore_link:
- chart_type="xy", kind="scatter": Scatter plot for correlation analysis - chart_type="xy", kind="scatter": Scatter plot for correlation analysis
- chart_type="big_number": Big Number display (single metric, header only) - chart_type="big_number": Big Number display (single metric, header only)
- chart_type="big_number", show_trendline=True, - chart_type="big_number", show_trendline=True,
temporal_column="<date_col>", aggregation="sum": Big Number with trendline temporal_column="<date_col>": Big Number with trendline
(aggregation controls how the value is computed from trendline data points;
default when omitted is "LAST_VALUE" — most recent point only.
Use aggregation="sum" for all-time totals, "mean" for averages, "max"/"min" for extremes.
DIAGNOSIS: if a Big Number with Trendline shows wrong values, check
form_data["aggregation"] — missing/LAST_VALUE means the chart shows only the last data
point, not a total. Fix by calling update_chart with a complete Big Number config:
chart_type="big_number", metric=<metric>, show_trendline=True,
temporal_column=<date_col>, aggregation="sum". update_chart requires the full
config — omitting chart_type or metric causes a validation error.)
- chart_type="table": Data table for detailed views - chart_type="table": Data table for detailed views
- chart_type="table", viz_type="ag-grid-table": Interactive AG Grid table - chart_type="table", viz_type="ag-grid-table": Interactive AG Grid table
- chart_type="pie": Pie chart for proportional data (set donut=True for donut) - chart_type="pie": Pie chart for proportional data (set donut=True for donut)

View File

@@ -859,9 +859,6 @@ def map_big_number_config(config: BigNumberChartConfig) -> Dict[str, Any]:
if config.time_format: if config.time_format:
form_data["time_format"] = config.time_format form_data["time_format"] = config.time_format
if config.aggregation is not None:
form_data["aggregation"] = config.aggregation
_add_adhoc_filters(form_data, config.filters) _add_adhoc_filters(form_data, config.filters)
return form_data return form_data

View File

@@ -1417,32 +1417,6 @@ class BigNumberChartConfig(UnknownFieldCheckMixin):
), ),
ge=1, ge=1,
) )
aggregation: (
Literal["LAST_VALUE", "sum", "mean", "min", "max", "median", "raw"] | None
) = Field(
None,
description=(
"How the single big-number value is computed from the trendline "
"data points. Only applies when show_trendline=True. "
"Options: "
"'sum' = Total (Sum) — add all data points; use for all-time totals. "
"'LAST_VALUE' = most recent data point "
"(frontend default when this field is absent). "
"'mean' = Average (Mean). "
"'min' = Minimum. "
"'max' = Maximum. "
"'median' = Median. "
"'raw' = Overall value — single aggregate across the full period; best for "
"non-additive metrics like ratios, averages, or distinct counts. "
"DIAGNOSIS: if a Big Number with Trendline shows an unexpectedly low value "
"(e.g. yesterday's revenue instead of all-time total), "
"inspect form_data['aggregation'] "
"— when absent or 'LAST_VALUE' the chart shows only the last data point. "
"Fix by setting aggregation='sum'. "
"IMPORTANT: when updating aggregation, always include "
"show_trendline=True and temporal_column to preserve the trendline."
),
)
filters: list[FilterConfig] | None = Field( filters: list[FilterConfig] | None = Field(
None, None,
description="Filters to apply", description="Filters to apply",
@@ -1463,13 +1437,6 @@ class BigNumberChartConfig(UnknownFieldCheckMixin):
"Period comparison is only available for " "Period comparison is only available for "
"trendline charts." "trendline charts."
) )
if self.aggregation and not self.show_trendline:
raise ValueError(
"aggregation requires show_trendline=True. "
"The aggregation field only applies to Big Number with "
"Trendline charts. Set show_trendline=True and provide "
"a temporal_column, or omit aggregation."
)
return self return self
@model_validator(mode="after") @model_validator(mode="after")

View File

@@ -115,14 +115,6 @@ _CHART_EXAMPLES: Dict[str, list[Dict[str, Any]]] = {
"chart_type": "big_number", "chart_type": "big_number",
"metric": {"name": "revenue", "aggregate": "SUM"}, "metric": {"name": "revenue", "aggregate": "SUM"},
}, },
{
"chart_type": "big_number",
"metric": {"name": "revenue", "aggregate": "SUM"},
"temporal_column": "order_date",
"show_trendline": True,
"aggregation": "sum",
"time_grain": "P1D",
},
], ],
} }

View File

@@ -25,7 +25,7 @@ import sqlalchemy as sa
from alembic import op from alembic import op
from flask import current_app from flask import current_app
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import lazyload, Session from sqlalchemy.orm import Session
from superset import db, security_manager from superset import db, security_manager
from superset.db_engine_specs.base import GenericDBException from superset.db_engine_specs.base import GenericDBException
@@ -379,15 +379,7 @@ def upgrade_catalog_perms(engines: set[str] | None = None) -> None:
bind = op.get_bind() bind = op.get_bind()
session = db.Session(bind=bind) session = db.Session(bind=bind)
# The Database model has an eager-loaded (``lazy="joined"``) ``ssh_tunnel`` for database in session.query(Database).all():
# backref. Eager-loading it here would SELECT every column on ``ssh_tunnels``,
# including columns added by later migrations that do not yet exist at the
# revision this helper runs in (e.g. on a fresh DB upgraded in one pass). The
# catalog upgrade only needs scalar ``Database`` columns, so disable the eager
# join to keep the query schema-safe across migration revisions.
for database in (
session.query(Database).options(lazyload(Database.ssh_tunnel)).all()
):
db_engine_spec = database.db_engine_spec db_engine_spec = database.db_engine_spec
if ( if (
engines and db_engine_spec.engine not in engines engines and db_engine_spec.engine not in engines
@@ -584,11 +576,7 @@ def downgrade_catalog_perms(engines: set[str] | None = None) -> None:
bind = op.get_bind() bind = op.get_bind()
session = db.Session(bind=bind) session = db.Session(bind=bind)
# See upgrade_catalog_perms: avoid eager-loading the ``ssh_tunnel`` backref so the for database in session.query(Database).all():
# query stays schema-safe across migration revisions.
for database in (
session.query(Database).options(lazyload(Database.ssh_tunnel)).all()
):
db_engine_spec = database.db_engine_spec db_engine_spec = database.db_engine_spec
if ( if (
engines and db_engine_spec.engine not in engines engines and db_engine_spec.engine not in engines

View File

@@ -1,50 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""add server_host_key to ssh_tunnels
Adds a nullable ``server_host_key`` column to the ``ssh_tunnels`` table. It stores the
expected SSH server host key in authorized-key form (e.g. "ssh-ed25519 AAAA...") so
operators can opt in to verifying the SSH server's host key before a tunnel is opened.
This is a public key and is stored in plaintext (not encrypted). The column is
nullable, so existing tunnels are unaffected.
Revision ID: 78a40c08b4be
Revises: b7c9d1e2f3a4
Create Date: 2026-06-03 10:00:00.000000
"""
import sqlalchemy as sa
from superset.migrations.shared.utils import add_columns, drop_columns
# revision identifiers, used by Alembic.
revision = "78a40c08b4be"
down_revision = "b7c9d1e2f3a4"
def upgrade() -> None:
"""Add the nullable ``server_host_key`` column to ``ssh_tunnels``."""
add_columns(
"ssh_tunnels",
sa.Column("server_host_key", sa.Text(), nullable=True),
)
def downgrade() -> None:
"""Drop the ``server_host_key`` column from ``ssh_tunnels``."""
drop_columns("ssh_tunnels", "server_host_key")

View File

@@ -119,5 +119,5 @@ def test_database_filter(mocker: MockerFixture) -> None:
) )
assert ( assert (
str(compiled_query) str(compiled_query)
== "SELECT dbs.uuid, dbs.created_on, dbs.changed_on, dbs.id, dbs.verbose_name, dbs.database_name, dbs.sqlalchemy_uri, dbs.password, dbs.cache_timeout, dbs.select_as_create_table_as, dbs.expose_in_sqllab, dbs.configuration_method, dbs.allow_run_async, dbs.allow_file_upload, dbs.allow_ctas, dbs.allow_cvas, dbs.allow_dml, dbs.force_ctas_schema, dbs.extra, dbs.encrypted_extra, dbs.impersonate_user, dbs.server_cert, dbs.is_managed_externally, dbs.external_url, dbs.created_by_fk, dbs.changed_by_fk, ssh_tunnels_1.uuid AS uuid_1, ssh_tunnels_1.created_on AS created_on_1, ssh_tunnels_1.changed_on AS changed_on_1, ssh_tunnels_1.extra_json, ssh_tunnels_1.id AS id_1, ssh_tunnels_1.database_id, ssh_tunnels_1.server_address, ssh_tunnels_1.server_port, ssh_tunnels_1.username, ssh_tunnels_1.password AS password_1, ssh_tunnels_1.private_key, ssh_tunnels_1.private_key_password, ssh_tunnels_1.server_host_key, ssh_tunnels_1.created_by_fk AS created_by_fk_1, ssh_tunnels_1.changed_by_fk AS changed_by_fk_1 \nFROM dbs LEFT OUTER JOIN ssh_tunnels AS ssh_tunnels_1 ON dbs.id = ssh_tunnels_1.database_id \nWHERE '[' || dbs.database_name || '].(id:' || CAST(dbs.id AS VARCHAR) || ')' IN ('[my_db].(id:42)', '[my_other_db].(id:43)') OR dbs.database_name IN ('my_db', 'my_other_db', 'third_db')" # noqa: E501 == "SELECT dbs.uuid, dbs.created_on, dbs.changed_on, dbs.id, dbs.verbose_name, dbs.database_name, dbs.sqlalchemy_uri, dbs.password, dbs.cache_timeout, dbs.select_as_create_table_as, dbs.expose_in_sqllab, dbs.configuration_method, dbs.allow_run_async, dbs.allow_file_upload, dbs.allow_ctas, dbs.allow_cvas, dbs.allow_dml, dbs.force_ctas_schema, dbs.extra, dbs.encrypted_extra, dbs.impersonate_user, dbs.server_cert, dbs.is_managed_externally, dbs.external_url, dbs.created_by_fk, dbs.changed_by_fk, ssh_tunnels_1.uuid AS uuid_1, ssh_tunnels_1.created_on AS created_on_1, ssh_tunnels_1.changed_on AS changed_on_1, ssh_tunnels_1.extra_json, ssh_tunnels_1.id AS id_1, ssh_tunnels_1.database_id, ssh_tunnels_1.server_address, ssh_tunnels_1.server_port, ssh_tunnels_1.username, ssh_tunnels_1.password AS password_1, ssh_tunnels_1.private_key, ssh_tunnels_1.private_key_password, ssh_tunnels_1.created_by_fk AS created_by_fk_1, ssh_tunnels_1.changed_by_fk AS changed_by_fk_1 \nFROM dbs LEFT OUTER JOIN ssh_tunnels AS ssh_tunnels_1 ON dbs.id = ssh_tunnels_1.database_id \nWHERE '[' || dbs.database_name || '].(id:' || CAST(dbs.id AS VARCHAR) || ')' IN ('[my_db].(id:42)', '[my_other_db].(id:43)') OR dbs.database_name IN ('my_db', 'my_other_db', 'third_db')" # noqa: E501
) )

View File

@@ -14,44 +14,11 @@
# KIND, either express or implied. See the License for the # KIND, either express or implied. See the License for the
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
from unittest.mock import Mock, patch from unittest.mock import Mock
import paramiko
import pytest
import sshtunnel import sshtunnel
from superset.commands.database.ssh_tunnel.exceptions import ( from superset.extensions.ssh import SSHManagerFactory
SSHTunnelHostKeyVerificationError,
)
from superset.extensions.ssh import SSHManager, SSHManagerFactory
def _make_manager(strict: bool = False) -> SSHManager:
"""Build an ``SSHManager`` test instance with configurable strict checking."""
app = Mock()
app.config = {
"SSH_TUNNEL_MAX_RETRIES": 2,
"SSH_TUNNEL_LOCAL_BIND_ADDRESS": "127.0.0.1",
"SSH_TUNNEL_TIMEOUT_SEC": 123.0,
"SSH_TUNNEL_PACKET_TIMEOUT_SEC": 321.0,
"SSH_TUNNEL_MANAGER_CLASS": "superset.extensions.ssh.SSHManager",
"SSH_TUNNEL_STRICT_HOST_KEY_CHECKING": strict,
}
return SSHManager(app)
def _authorized_key(key: paramiko.PKey) -> str:
"""Render a paramiko key in authorized-key (``"<type> <base64>"``) form."""
return f"{key.get_name()} {key.get_base64()}"
def _ssh_tunnel(server_host_key: str | None) -> Mock:
"""Create a mocked SSH tunnel with server connection fields populated."""
tunnel = Mock()
tunnel.server_address = "ssh.example.com"
tunnel.server_port = 22
tunnel.server_host_key = server_host_key
return tunnel
def test_ssh_tunnel_timeout_setting() -> None: def test_ssh_tunnel_timeout_setting() -> None:
@@ -67,199 +34,3 @@ def test_ssh_tunnel_timeout_setting() -> None:
factory.init_app(app) factory.init_app(app)
assert sshtunnel.TUNNEL_TIMEOUT == 123.0 assert sshtunnel.TUNNEL_TIMEOUT == 123.0
assert sshtunnel.SSH_TIMEOUT == 321.0 assert sshtunnel.SSH_TIMEOUT == 321.0
@patch("superset.extensions.ssh.socket.create_connection")
@patch("superset.extensions.ssh.paramiko.Transport")
def test_verify_host_key_match(
mock_transport_cls: Mock, mock_create_connection: Mock
) -> None:
"""The server presents the same key we expect: verification passes."""
server_key = paramiko.RSAKey.generate(2048)
manager = _make_manager(strict=False)
tunnel = _ssh_tunnel(_authorized_key(server_key))
transport = mock_transport_cls.return_value
transport.get_remote_server_key.return_value = server_key
result = manager._verify_host_key(tunnel) # should not raise
# The TCP connect is bounded by an explicit timeout, and the resulting
# socket is handed to Transport.
mock_create_connection.assert_called_once_with(
("ssh.example.com", 22), timeout=321.0
)
mock_transport_cls.assert_called_once_with(mock_create_connection.return_value)
transport.start_client.assert_called_once()
transport.close.assert_called_once()
# The parsed expected key is returned so the caller can pin it on the tunnel.
assert result == server_key
@patch("superset.extensions.ssh.socket.create_connection")
@patch("superset.extensions.ssh.paramiko.Transport")
def test_verify_host_key_mismatch_raises(
mock_transport_cls: Mock, mock_create_connection: Mock
) -> None:
"""The server presents a different key than expected: verification fails."""
expected_key = paramiko.RSAKey.generate(2048)
presented_key = paramiko.RSAKey.generate(2048)
manager = _make_manager(strict=False)
tunnel = _ssh_tunnel(_authorized_key(expected_key))
transport = mock_transport_cls.return_value
transport.get_remote_server_key.return_value = presented_key
with pytest.raises(SSHTunnelHostKeyVerificationError):
manager._verify_host_key(tunnel)
mock_create_connection.assert_called_once()
transport.close.assert_called_once()
@patch("superset.extensions.ssh.socket.create_connection")
def test_verify_host_key_connect_failure_raises(
mock_create_connection: Mock,
) -> None:
"""A bounded TCP connect failure surfaces as a host-key verification error."""
manager = _make_manager(strict=False)
server_key = paramiko.RSAKey.generate(2048)
tunnel = _ssh_tunnel(_authorized_key(server_key))
mock_create_connection.side_effect = OSError("connection refused")
with pytest.raises(SSHTunnelHostKeyVerificationError):
manager._verify_host_key(tunnel)
@patch("superset.extensions.ssh.paramiko.Transport")
def test_verify_host_key_unset_non_strict_skips(mock_transport_cls: Mock) -> None:
"""Back-compat: no expected key + strict checking off => no verification at all."""
manager = _make_manager(strict=False)
tunnel = _ssh_tunnel(None)
assert manager._verify_host_key(tunnel) is None # should not raise
mock_transport_cls.assert_not_called()
@patch("superset.extensions.ssh.paramiko.Transport")
def test_verify_host_key_unset_strict_raises(mock_transport_cls: Mock) -> None:
"""Fail-closed: no expected key + strict checking on => reject."""
manager = _make_manager(strict=True)
tunnel = _ssh_tunnel(None)
with pytest.raises(SSHTunnelHostKeyVerificationError):
manager._verify_host_key(tunnel)
mock_transport_cls.assert_not_called()
@patch("superset.extensions.ssh.socket.create_connection")
@patch("superset.extensions.ssh.paramiko.Transport")
def test_verify_host_key_match_ignores_comment_and_whitespace(
mock_transport_cls: Mock,
mock_create_connection: Mock,
) -> None:
# The stored key may carry a trailing comment and extra whitespace.
server_key = paramiko.RSAKey.generate(2048)
manager = _make_manager(strict=False)
stored = f" {_authorized_key(server_key)} user@host "
tunnel = _ssh_tunnel(stored)
transport = mock_transport_cls.return_value
transport.get_remote_server_key.return_value = server_key
manager._verify_host_key(tunnel) # should not raise
# Whitespace/comment stripping must not short-circuit verification: the
# bounded TCP connect and Transport handshake still run as in the plain
# match case.
mock_create_connection.assert_called_once_with(
("ssh.example.com", 22), timeout=321.0
)
mock_transport_cls.assert_called_once_with(mock_create_connection.return_value)
transport.start_client.assert_called_once()
transport.close.assert_called_once()
def test_verify_host_key_invalid_expected_raises() -> None:
# A malformed expected key is rejected before any network connection.
manager = _make_manager(strict=False)
tunnel = _ssh_tunnel("not-a-valid-key")
with pytest.raises(SSHTunnelHostKeyVerificationError):
manager._verify_host_key(tunnel)
def test_verify_host_key_unknown_key_type_raises() -> None:
"""An unsupported key type is wrapped in the verification error, not leaked."""
manager = _make_manager(strict=False)
server_key = paramiko.RSAKey.generate(2048)
tunnel = _ssh_tunnel(f"ssh-bogus {server_key.get_base64()}")
with pytest.raises(SSHTunnelHostKeyVerificationError):
manager._verify_host_key(tunnel)
@patch("superset.extensions.ssh.sshtunnel.open_tunnel")
@patch("superset.extensions.ssh.socket.create_connection")
@patch("superset.extensions.ssh.paramiko.Transport")
def test_create_tunnel_pins_verified_host_key(
mock_transport_cls: Mock,
mock_create_connection: Mock,
mock_open_tunnel: Mock,
) -> None:
"""A verified expected key is also pinned on the tunnel's own connection.
When an expected host key is configured and verified, it is also pinned on the
tunnel's own connection (``ssh_host_key``) so paramiko verifies the host that
actually carries traffic on the same transport — closing the probe-vs-tunnel
TOCTOU gap rather than trusting only the pre-flight probe.
"""
server_key = paramiko.RSAKey.generate(2048)
manager = _make_manager(strict=False)
tunnel = _ssh_tunnel(_authorized_key(server_key))
tunnel.username = "user"
tunnel.password = None
tunnel.private_key = None
mock_transport_cls.return_value.get_remote_server_key.return_value = server_key
manager.create_tunnel(tunnel, "postgresql://u:p@db:5432/ex")
_, kwargs = mock_open_tunnel.call_args
assert kwargs["ssh_host_key"] == server_key
@patch("superset.extensions.ssh.sshtunnel.open_tunnel")
def test_create_tunnel_without_host_key_does_not_pin(mock_open_tunnel: Mock) -> None:
# No expected key configured (non-strict): nothing is pinned, preserving the
# prior behavior.
manager = _make_manager(strict=False)
tunnel = _ssh_tunnel(None)
tunnel.username = "user"
tunnel.password = None
tunnel.private_key = None
manager.create_tunnel(tunnel, "postgresql://u:p@db:5432/ex")
_, kwargs = mock_open_tunnel.call_args
assert "ssh_host_key" not in kwargs
def test_ssh_tunnel_schema_round_trips_server_host_key() -> None:
"""The schema accepts and preserves the public host key field."""
from superset.databases.schemas import DatabaseSSHTunnel
server_key = paramiko.RSAKey.generate(2048)
authorized = _authorized_key(server_key)
payload = {
"server_address": "ssh.example.com",
"server_port": 22,
"username": "user",
"password": "secret",
"server_host_key": authorized,
}
loaded = DatabaseSSHTunnel().load(payload)
assert loaded["server_host_key"] == authorized

View File

@@ -177,17 +177,6 @@ class TestBigNumberChartConfig:
compare_lag=1, compare_lag=1,
) )
def test_aggregation_requires_trendline(self) -> None:
"""aggregation without show_trendline=True must raise ValidationError."""
with pytest.raises(
ValidationError, match="aggregation requires show_trendline"
):
BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
aggregation="sum",
)
def test_with_filters(self) -> None: def test_with_filters(self) -> None:
config = BigNumberChartConfig( config = BigNumberChartConfig(
chart_type="big_number", chart_type="big_number",
@@ -199,36 +188,6 @@ class TestBigNumberChartConfig:
assert config.filters is not None assert config.filters is not None
assert len(config.filters) == 1 assert len(config.filters) == 1
def test_with_aggregation_sum(self) -> None:
"""aggregation='sum' is accepted when show_trendline=True."""
config = BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
temporal_column="ds",
show_trendline=True,
aggregation="sum",
)
assert config.aggregation == "sum"
def test_with_aggregation_last_value(self) -> None:
"""aggregation='LAST_VALUE' is accepted when show_trendline=True."""
config = BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
temporal_column="ds",
show_trendline=True,
aggregation="LAST_VALUE",
)
assert config.aggregation == "LAST_VALUE"
def test_aggregation_defaults_to_none(self) -> None:
"""aggregation field is None when omitted."""
config = BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
)
assert config.aggregation is None
def test_extra_fields_forbidden(self) -> None: def test_extra_fields_forbidden(self) -> None:
with pytest.raises(ValueError, match="Unknown field 'unknown_field'"): with pytest.raises(ValueError, match="Unknown field 'unknown_field'"):
BigNumberChartConfig( BigNumberChartConfig(
@@ -348,52 +307,6 @@ class TestMapBigNumberConfig:
assert "time_grain_sqla" not in form_data assert "time_grain_sqla" not in form_data
assert "start_y_axis_at_zero" not in form_data assert "start_y_axis_at_zero" not in form_data
def test_with_aggregation_sum(self) -> None:
"""aggregation='sum' is written to form_data for trendline charts."""
config = BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
temporal_column="order_date",
show_trendline=True,
aggregation="sum",
)
form_data = map_big_number_config(config)
assert form_data["aggregation"] == "sum"
def test_with_aggregation_last_value(self) -> None:
"""aggregation='LAST_VALUE' is written to form_data for trendline charts."""
config = BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
temporal_column="order_date",
show_trendline=True,
aggregation="LAST_VALUE",
)
form_data = map_big_number_config(config)
assert form_data["aggregation"] == "LAST_VALUE"
def test_aggregation_absent_when_not_set(self) -> None:
"""aggregation key is absent from form_data when config.aggregation is None."""
config = BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
temporal_column="order_date",
show_trendline=True,
)
form_data = map_big_number_config(config)
assert "aggregation" not in form_data
def test_aggregation_not_allowed_for_big_number_total(self) -> None:
"""aggregation is rejected when show_trendline=False (big_number_total)."""
with pytest.raises(
ValidationError, match="aggregation requires show_trendline"
):
BigNumberChartConfig(
chart_type="big_number",
metric=ColumnRef(name="revenue", aggregate="SUM"),
aggregation="sum",
)
class TestMapConfigToFormDataBigNumber: class TestMapConfigToFormDataBigNumber:
"""Test map_config_to_form_data dispatch for big number.""" """Test map_config_to_form_data dispatch for big number."""