From e0805136661d89340dccd17dba4fe4282398f6ce Mon Sep 17 00:00:00 2001 From: Luiz Felipe Machado <56140722+luizfelmach@users.noreply.github.com> Date: Wed, 22 Apr 2026 06:20:32 -0300 Subject: [PATCH 01/63] feat: add Envoy API gateway for self-hosted Docker Compose (#43838) --- docker/docker-compose.envoy.yml | 51 + docker/volumes/api/envoy/cds.yaml | 223 ++++ docker/volumes/api/envoy/docker-entrypoint.sh | 34 + docker/volumes/api/envoy/envoy.yaml | 27 + docker/volumes/api/envoy/lds.template.yaml | 989 ++++++++++++++++++ docker/volumes/logs/vector.yml | 2 +- 6 files changed, 1325 insertions(+), 1 deletion(-) create mode 100644 docker/docker-compose.envoy.yml create mode 100644 docker/volumes/api/envoy/cds.yaml create mode 100755 docker/volumes/api/envoy/docker-entrypoint.sh create mode 100644 docker/volumes/api/envoy/envoy.yaml create mode 100644 docker/volumes/api/envoy/lds.template.yaml diff --git a/docker/docker-compose.envoy.yml b/docker/docker-compose.envoy.yml new file mode 100644 index 00000000000..31553524031 --- /dev/null +++ b/docker/docker-compose.envoy.yml @@ -0,0 +1,51 @@ +# Envoy override for Kong +# Usage: docker compose -f docker-compose.yml -f docker-compose.envoy.yml up + +services: + # Disable the original Kong service + kong: + profiles: + - disabled + + # Rewire dependencies that require Kong to Envoy + functions: + depends_on: !override + api-gw: + condition: service_healthy + + # Envoy API gateway + api-gw: + container_name: supabase-envoy + image: envoyproxy/envoy:v1.37.2 + restart: unless-stopped + ports: + - ${KONG_HTTP_PORT}:8000/tcp + volumes: + - ./volumes/api/envoy/envoy.yaml:/etc/envoy/envoy.yaml:ro + - ./volumes/api/envoy/cds.yaml:/etc/envoy/cds.yaml:ro + - ./volumes/api/envoy/lds.template.yaml:/etc/envoy/lds.template.yaml:ro + - ./volumes/api/envoy/docker-entrypoint.sh:/docker-entrypoint.sh:ro + depends_on: + studio: + condition: service_healthy + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-} + SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-} + ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-} + SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + entrypoint: ["/bin/sh", "/docker-entrypoint.sh"] + healthcheck: + # Using a TCP port check because this image does not include curl or wget. + test: ["CMD-SHELL", "timeout 1 bash -c ' /etc/envoy/lds.yaml + +if [ -n "$SUPABASE_SECRET_KEY" ] && \ + [ -n "$SUPABASE_PUBLISHABLE_KEY" ] && \ + [ -n "$SERVICE_ROLE_KEY_ASYMMETRIC" ] && \ + [ -n "$ANON_KEY_ASYMMETRIC" ]; then + echo "Envoy sb_ key translation enabled" +else + echo "Envoy running in legacy API key mode (sb_ keys disabled)" +fi + +echo "Envoy configuration generated successfully" +echo "Starting Envoy..." + +# Start Envoy +exec envoy -c /etc/envoy/envoy.yaml "$@" diff --git a/docker/volumes/api/envoy/envoy.yaml b/docker/volumes/api/envoy/envoy.yaml new file mode 100644 index 00000000000..bf3dd4ebf73 --- /dev/null +++ b/docker/volumes/api/envoy/envoy.yaml @@ -0,0 +1,27 @@ +dynamic_resources: + cds_config: + path_config_source: + path: /etc/envoy/cds.yaml + resource_api_version: V3 + lds_config: + path_config_source: + path: /etc/envoy/lds.yaml + resource_api_version: V3 + +node: + cluster: supabase_cluster + id: supabase_node + +overload_manager: + resource_monitors: + - name: envoy.resource_monitors.global_downstream_max_connections + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig + max_active_downstream_connections: 30000 + +admin: + address: + socket_address: + address: 127.0.0.1 + port_value: 9901 diff --git a/docker/volumes/api/envoy/lds.template.yaml b/docker/volumes/api/envoy/lds.template.yaml new file mode 100644 index 00000000000..26038d78060 --- /dev/null +++ b/docker/volumes/api/envoy/lds.template.yaml @@ -0,0 +1,989 @@ +resources: + - '@type': type.googleapis.com/envoy.config.listener.v3.Listener + name: supabase + per_connection_buffer_limit_bytes: 32768 # 32 KiB + + address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + normalize_path: true + merge_slashes: true + path_with_escaped_slashes_action: REJECT_REQUEST + use_remote_address: true + common_http_protocol_options: + headers_with_underscores_action: REJECT_REQUEST + upgrade_configs: + - upgrade_type: websocket + access_log: + - name: envoy.access_loggers.stdout + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + log_format: + text_format_source: + inline_string: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT% - - [%START_TIME(%d/%b/%Y:%H:%M:%S %z)%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %BYTES_SENT% \"%REQ(REFERER)%\" \"%REQ(USER-AGENT)%\"\n" + + route_config: + name: supabase_route + virtual_hosts: + - name: supabase_host + domains: + - '*' + cors: + allow_origin_string_match: + - safe_regex: + regex: ".*" + allow_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD,CONNECT,TRACE" + allow_headers: "*" + expose_headers: "*" + max_age: "3600" + request_headers_to_add: + - header: + key: X-Forwarded-Host + value: "%REQ(:AUTHORITY)%" + append_action: ADD_IF_ABSENT + - header: + key: X-Forwarded-Port + value: "%DOWNSTREAM_LOCAL_PORT%" + append_action: ADD_IF_ABSENT + routes: + - match: + prefix: /auth/v1/verify + route: + cluster: auth + prefix_rewrite: /verify + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /auth/v1/verify + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /auth/v1/callback + route: + cluster: auth + prefix_rewrite: /callback + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /auth/v1/callback + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /auth/v1/authorize + route: + cluster: auth + prefix_rewrite: /authorize + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /auth/v1/authorize + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /auth/v1/.well-known/jwks.json + route: + cluster: auth + prefix_rewrite: /.well-known/jwks.json + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /auth/v1/.well-known/jwks.json + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /.well-known/oauth-authorization-server + route: + cluster: auth + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /.well-known/oauth-authorization-server + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /sso/saml/acs + route: + cluster: auth + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /sso/saml/acs + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /sso/saml/metadata + route: + cluster: auth + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /sso/saml/metadata + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - name: functions-v1-all + match: + prefix: /functions/v1/ + route: + cluster: functions + prefix_rewrite: / + timeout: 150s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /functions/v1/ + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /storage/v1/ + route: + cluster: storage + prefix_rewrite: / + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /storage/v1 + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + - name: auth-v1-protected + match: + prefix: /auth/v1/ + route: + cluster: auth + prefix_rewrite: / + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /auth/v1/ + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + + - name: rest-v1-protected + match: + prefix: /rest/v1/ + route: + cluster: rest + prefix_rewrite: / + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /rest/v1/ + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + + - name: graphql-v1-protected + match: + prefix: /graphql/v1 + route: + cluster: rest + prefix_rewrite: /rpc/graphql + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /graphql/v1 + append_action: ADD_IF_ABSENT + - header: + key: Content-Profile + value: graphql_public + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + + - name: realtime-v1-api-protected + match: + prefix: /realtime/v1/api + route: + cluster: realtime + prefix_rewrite: /api + timeout: 30s + host_rewrite_literal: realtime-dev.supabase-realtime + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /realtime/v1/api + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + + - name: realtime-v1-ws-protected + match: + prefix: /realtime/v1/ + route: + cluster: realtime + prefix_rewrite: /socket/ + timeout: 30s + host_rewrite_literal: realtime-dev.supabase-realtime + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /realtime/v1/ + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + + - name: pg-protected + match: + prefix: /pg/ + route: + cluster: meta + prefix_rewrite: / + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /pg/ + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + + - match: + prefix: /api/mcp + route: + cluster: studio + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /api/mcp + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: DENY + policies: + deny_all: + permissions: + - any: true + principals: + - any: true + + - match: + prefix: /mcp + route: + cluster: studio + prefix_rewrite: /api/mcp + timeout: 30s + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: /mcp + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.basic_auth: + '@type': >- + type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: DENY + policies: + deny_all: + permissions: + - any: true + principals: + - any: true + # Enable local access (danger zone!) + # 1. Replace the `rbac` block above with the one below. + # 2. Adjust the IP ranges in `principals`. + # rbac: + # rules: + # action: ALLOW + # policies: + # allow_local: + # permissions: + # - any: true + # principals: + # - direct_remote_ip: + # address_prefix: 127.0.0.1 + # prefix_len: 32 + # - direct_remote_ip: + # address_prefix: ::1 + # prefix_len: 128 + + - match: + prefix: / + route: + cluster: studio + timeout: 30s + request_headers_to_remove: + - authorization + request_headers_to_add: + - header: + key: X-Forwarded-Prefix + value: / + append_action: ADD_IF_ABSENT + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: ALLOW + policies: + allow_all: + permissions: + - any: true + principals: + - any: true + + http_filters: + - name: envoy.filters.http.cors + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors + + - name: envoy.filters.http.basic_auth + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth + users: + inline_string: '${DASHBOARD_BASIC_AUTH}' + + # Copies ?apikey=... from the URL into the apikey header when clients omit the header. + - name: envoy.filters.http.lua + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local FUNCTIONS_ROUTE = "functions-v1-all" + local FUNCTIONS_PREFIX = "/functions/v1/" + + local function is_functions_request(request_handle, headers) + if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then + return true + end + + local path = headers:get(":path") + if path == nil then + return false + end + + return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX + end + + function envoy_on_request(request_handle) + local headers = request_handle:headers() + if is_functions_request(request_handle, headers) then + return + end + + if headers:get("apikey") ~= nil then + return + end + + local path = headers:get(":path") + local query_start = string.find(path, "?", 1, true) + if query_start == nil then + return + end + + local query = string.sub(path, query_start + 1) + for key, value in string.gmatch(query, "([^&]+)=([^&]*)") do + if key == "apikey" and value ~= "" then + headers:add("apikey", value) + return + end + end + end + + # Translates the query parameter apikey into the matching internal JWT and rewrites the URL so only JWTs propagate downstream. + - name: envoy.filters.http.lua + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local FUNCTIONS_ROUTE = "functions-v1-all" + local FUNCTIONS_PREFIX = "/functions/v1/" + local SECRET_KEY = "${SUPABASE_SECRET_KEY}" + local PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}" + local SERVICE_ROLE_JWT = "${SERVICE_ROLE_KEY_ASYMMETRIC}" + local ANON_JWT = "${ANON_KEY_ASYMMETRIC}" + local TRANSLATION_ENABLED = SECRET_KEY ~= "" and PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_JWT ~= "" and ANON_JWT ~= "" + + local function is_functions_request(request_handle, headers) + if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then + return true + end + + local path = headers:get(":path") + if path == nil then + return false + end + + return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX + end + + local function translate_apikey(apikey) + if apikey == nil or apikey == "" then + return nil + end + + if not TRANSLATION_ENABLED then + return nil + end + + if apikey == SECRET_KEY then + return SERVICE_ROLE_JWT + end + + if apikey == PUBLISHABLE_KEY then + return ANON_JWT + end + + return nil + end + + local function extract_query_apikey(path) + if path == nil or path == "" then + return nil + end + + local query_start = string.find(path, "?", 1, true) + if query_start == nil then + return nil + end + + local query = string.sub(path, query_start + 1) + for key, value in string.gmatch(query, "([^&]+)=([^&]*)") do + if key == "apikey" and value ~= "" then + return value + end + end + + return nil + end + + local function replace_query_apikey(path, new_value) + if path == nil or path == "" or new_value == nil or new_value == "" then + return nil + end + + local query_start = string.find(path, "?", 1, true) + if query_start == nil then + return nil + end + + local base = string.sub(path, 1, query_start) + local query = string.sub(path, query_start + 1) + local updated = {} + local replaced = false + + for part in string.gmatch(query, "([^&]+)") do + local key, value = string.match(part, "([^=]+)=(.*)") + if key == "apikey" then + part = key .. "=" .. new_value + replaced = true + end + table.insert(updated, part) + end + + if not replaced then + return nil + end + + return base .. table.concat(updated, "&") + end + + function envoy_on_request(request_handle) + local headers = request_handle:headers() + if is_functions_request(request_handle, headers) then + return + end + + local path = headers:get(":path") + local apikey = extract_query_apikey(path) + local translated = translate_apikey(apikey) + + if translated == nil then + return + end + + headers:replace("apikey", translated) + + local rewritten_path = replace_query_apikey(path, translated) + if rewritten_path ~= nil then + headers:replace(":path", rewritten_path) + end + end + + # Translates an apikey header into the appropriate internal JWT for downstream RBAC checks. + - name: envoy.filters.http.lua + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local FUNCTIONS_ROUTE = "functions-v1-all" + local FUNCTIONS_PREFIX = "/functions/v1/" + local SECRET_KEY = "${SUPABASE_SECRET_KEY}" + local PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}" + local SERVICE_ROLE_JWT = "${SERVICE_ROLE_KEY_ASYMMETRIC}" + local ANON_JWT = "${ANON_KEY_ASYMMETRIC}" + local TRANSLATION_ENABLED = SECRET_KEY ~= "" and PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_JWT ~= "" and ANON_JWT ~= "" + + local function is_functions_request(request_handle, headers) + if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then + return true + end + + local path = headers:get(":path") + if path == nil then + return false + end + + return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX + end + + local function translate_apikey(apikey) + if apikey == nil or apikey == "" then + return nil + end + + if not TRANSLATION_ENABLED then + return nil + end + + if apikey == SECRET_KEY then + return SERVICE_ROLE_JWT + end + + if apikey == PUBLISHABLE_KEY then + return ANON_JWT + end + + return nil + end + + function envoy_on_request(request_handle) + local headers = request_handle:headers() + if is_functions_request(request_handle, headers) then + return + end + + local translated = translate_apikey(headers:get("apikey")) + if translated ~= nil and translated ~= "" then + headers:replace("apikey", translated) + end + end + + # Mirrors apikey into x-api-key for realtime WS compatibility. + - name: envoy.filters.http.lua + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local REALTIME_WS_ROUTE = "realtime-v1-ws-protected" + + function envoy_on_request(request_handle) + local route_name = request_handle:streamInfo():routeName() + if route_name ~= REALTIME_WS_ROUTE then + return + end + + local headers = request_handle:headers() + local apikey = headers:get("apikey") + if apikey == nil or apikey == "" then + return + end + + headers:replace("x-api-key", apikey) + end + + # Synthesizes an Authorization header (Bearer …) from apikey when callers don’t provide a real JWT header. + - name: envoy.filters.http.lua + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local FUNCTIONS_ROUTE = "functions-v1-all" + local FUNCTIONS_PREFIX = "/functions/v1/" + local REALTIME_WS_ROUTE = "realtime-v1-ws-protected" + + local function is_functions_request(request_handle, headers) + if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then + return true + end + + local path = headers:get(":path") + if path == nil then + return false + end + + return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX + end + + local function has_real_jwt(auth_header) + if auth_header == nil or auth_header == "" then + return false + end + + if string.sub(auth_header, 1, 7) ~= "Bearer " then + return false + end + + return string.sub(auth_header, 1, 10) ~= "Bearer sb_" + end + + local function format_authorization(value) + if value == nil or value == "" then + return nil + end + + if string.sub(value, 1, 7) == "Bearer " then + return value + end + + return "Bearer " .. value + end + + function envoy_on_request(request_handle) + local headers = request_handle:headers() + if request_handle:streamInfo():routeName() == REALTIME_WS_ROUTE then + return + end + + if is_functions_request(request_handle, headers) then + return + end + + if has_real_jwt(headers:get("authorization")) then + return + end + + local apikey = headers:get("apikey") + local authorization_value = format_authorization(apikey) + if authorization_value ~= nil then + headers:replace("authorization", authorization_value) + end + end + + # Returns 401 for missing/invalid API keys on protected API routes. + - name: envoy.filters.http.lua + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local ANON_KEY = "${ANON_KEY}" + local SERVICE_ROLE_KEY = "${SERVICE_ROLE_KEY}" + local ANON_KEY_ASYMMETRIC = "${ANON_KEY_ASYMMETRIC}" + local SERVICE_ROLE_KEY_ASYMMETRIC = "${SERVICE_ROLE_KEY_ASYMMETRIC}" + + local PROTECTED_ROUTES = { + ["auth-v1-protected"] = true, + ["rest-v1-protected"] = true, + ["graphql-v1-protected"] = true, + ["realtime-v1-api-protected"] = true, + ["realtime-v1-ws-protected"] = true, + ["pg-protected"] = true, + } + + local function is_protected_route(route_name) + if route_name == nil or route_name == "" then + return false + end + + return PROTECTED_ROUTES[route_name] == true + end + + local function is_valid_apikey(apikey) + if apikey == nil or apikey == "" then + return false + end + + if SERVICE_ROLE_KEY ~= "" and apikey == SERVICE_ROLE_KEY then + return true + end + + if ANON_KEY ~= "" and apikey == ANON_KEY then + return true + end + + if SERVICE_ROLE_KEY_ASYMMETRIC ~= "" and apikey == SERVICE_ROLE_KEY_ASYMMETRIC then + return true + end + + if ANON_KEY_ASYMMETRIC ~= "" and apikey == ANON_KEY_ASYMMETRIC then + return true + end + + return false + end + + function envoy_on_request(request_handle) + local headers = request_handle:headers() + local route_name = request_handle:streamInfo():routeName() + if not is_protected_route(route_name) then + return + end + + if is_valid_apikey(headers:get("apikey")) then + return + end + + request_handle:respond({ + [":status"] = "401", + ["content-type"] = "text/plain", + }, "Unauthorized") + end + + - name: envoy.filters.http.rbac + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: ALLOW + policies: + admin: + permissions: + - url_path: + path: + prefix: /pg/ + principals: + - header: + name: apikey + string_match: + exact: '${SERVICE_ROLE_KEY}' + - header: + name: apikey + string_match: + exact: '${SERVICE_ROLE_KEY_ASYMMETRIC}' + apikey: + permissions: + - url_path: + path: + prefix: /auth/v1/ + - url_path: + path: + prefix: /rest/v1/ + - url_path: + path: + prefix: /realtime/v1/api + - url_path: + path: + prefix: /realtime/v1/ + - url_path: + path: + prefix: /graphql/v1 + principals: + - header: + name: apikey + string_match: + exact: '${SERVICE_ROLE_KEY}' + - header: + name: apikey + string_match: + exact: '${ANON_KEY}' + - header: + name: apikey + string_match: + exact: '${SERVICE_ROLE_KEY_ASYMMETRIC}' + - header: + name: apikey + string_match: + exact: '${ANON_KEY_ASYMMETRIC}' + - name: envoy.filters.http.router + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/docker/volumes/logs/vector.yml b/docker/volumes/logs/vector.yml index d600bf28677..f63bfd8ad10 100644 --- a/docker/volumes/logs/vector.yml +++ b/docker/volumes/logs/vector.yml @@ -30,7 +30,7 @@ transforms: inputs: - project_logs route: - kong: '.appname == "supabase-kong"' + kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"' auth: '.appname == "supabase-auth"' rest: '.appname == "supabase-rest"' realtime: '.appname == "realtime-dev.supabase-realtime"' From cc4d985c401a37bd8bb3f7fdba4043c65d0ffdc7 Mon Sep 17 00:00:00 2001 From: Inder Singh <85822513+singh-inder@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:54:58 +0530 Subject: [PATCH 02/63] feat(docker): remove superuser access (#42975) --- .../NavigationMenu.constants.ts | 1 + .../self-hosting/remove-superuser-access.mdx | 75 +++++++++ docker/docker-compose.yml | 4 + docker/utils/reassign-owner.sh | 153 ++++++++++++++++++ 4 files changed, 233 insertions(+) create mode 100644 apps/docs/content/guides/self-hosting/remove-superuser-access.mdx create mode 100644 docker/utils/reassign-owner.sh diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 3e9a854c652..776fcec5926 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2897,6 +2897,7 @@ export const self_hosting: NavMenuConstant = { { name: 'Configure Phone Login & MFA', url: '/guides/self-hosting/self-hosted-phone-mfa' }, { name: 'Configure SAML 2.0 SSO', url: '/guides/self-hosting/self-hosted-saml-sso' }, { name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' }, + { name: 'Remove superuser access', url: '/guides/self-hosting/remove-superuser-access' }, ], }, { diff --git a/apps/docs/content/guides/self-hosting/remove-superuser-access.mdx b/apps/docs/content/guides/self-hosting/remove-superuser-access.mdx new file mode 100644 index 00000000000..a2b7c6d6d8d --- /dev/null +++ b/apps/docs/content/guides/self-hosting/remove-superuser-access.mdx @@ -0,0 +1,75 @@ +--- +title: 'Remove superuser access from Studio' +description: 'Learn how to switch from the supabase_admin to postgres role in self-hosted Supabase.' +subtitle: 'Learn how to switch from the supabase_admin to postgres role in self-hosted Supabase.' +--- + +## Overview + +In late 2022, Supabase introduced a security change in hosted projects that removed superuser access from the dashboard SQL editor and shifted ownership of user-created database objects away from `supabase_admin` toward the `postgres` role. +You can read more about it in the [official announcement](https://github.com/orgs/supabase/discussions/9314). + +However, this migration was never automatically applied to self-hosted Supabase instances. + +As a result: + +- Objects created via the dashboard may still be owned by `supabase_admin` +- Behavior differs from the Supabase platform +- Some migrations may fail when run as `postgres` + +This guide explains how to align your self-hosted Supabase instance with the security enhancements and ownership model used on the Supabase platform. + +## Changing the configuration + +### Step 1: Update database object ownership + +Use the provided script to reassign ownership of database objects in the `public` schema from `supabase_admin` to `postgres`. From the project directory containing `docker-compose.yml`, run: + +```sh +sh utils/reassign-owner.sh +``` + + + +This script only updates ownership for database objects in the `public` schema. Supabase-managed and custom schemas are not affected. + + + +### Step 2: Update environment variables in docker-compose.yml + +- In your `docker-compose.yml` configuration, uncomment the following line for the `studio` service to use the `postgres` role for read/write operations: + + ```yml name=docker-compose.yml + studio: + environment: + POSTGRES_USER_READ_WRITE: postgres + ``` + +- Locate the `meta` service environment variables and change the `PG_META_DB_USER` environment variable from `supabase_admin` to `postgres`: + + ```yml name=docker-compose.yml + meta: + environment: + PG_META_DB_USER: postgres + ``` + + + +Studio uses its own credentials to access Postgres via `postgres-meta`, so this change is only needed for backward compatibility and consistency. + + + +### Step 3: Restart Supabase + +```sh +docker compose down && docker compose up -d +``` + +## Verify roles + +After restarting your services, verify that Supabase Studio is now using the `postgres` role. Run the following query in the Supabase Studio SQL Editor: + +```sql +select current_user; +-- expected result: postgres +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index aa9e9cd0e6c..7bcf1a1c47d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -37,6 +37,10 @@ services: POSTGRES_HOST: ${POSTGRES_HOST} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + # See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access + #POSTGRES_USER_READ_WRITE: postgres + PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} diff --git a/docker/utils/reassign-owner.sh b/docker/utils/reassign-owner.sh new file mode 100644 index 00000000000..d13b1707337 --- /dev/null +++ b/docker/utils/reassign-owner.sh @@ -0,0 +1,153 @@ +#!/bin/sh +# +# Reassign ownership of public schema objects from supabase_admin to postgres. +# +# Context and documentation: +# https://supabase.com/docs/guides/self-hosting/remove-superuser-access +# +# Credits: +# Original version by Inder Singh. +# +# Usage: +# sh utils/reassign-owner.sh +# + +set -e + +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose not found." + exit 1 +fi + +# Check Postgres service +db_image_prefix="supabase.postgres:" + +compose_output=$(docker compose ps \ + --format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null | + grep -m1 "^$db_image_prefix" || true) + +if [ -z "$compose_output" ]; then + echo "Postgres container not found. Exiting." + exit 1 +fi + +db_srv_name=$(echo "$compose_output" | cut -f2) +db_srv_status=$(echo "$compose_output" | cut -f3) + +case "$db_srv_status" in + Up*) + ;; + *) + echo "Postgres container status: $db_srv_status" + echo "Exiting." + exit 1 + ;; +esac + +if ! test -t 0; then + echo "" + echo "Running non-interactively. Not reassigning ownership." + exit 0 +fi + +printf "Reassign public schema objects to postgres user? (y/N) " +read -r REPLY +case "$REPLY" in + [Yy]) + ;; + *) + echo "Canceled. Not reassigning ownership." + exit 0 + ;; +esac + +docker compose exec -T "$db_srv_name" psql -v ON_ERROR_STOP=1 -U supabase_admin -d postgres <<'EOF' +\echo 'Current supabase_admin-owned objects in public schema:' +SELECT c.relname, c.relkind, c.relowner::regrole +FROM pg_class c +WHERE c.relnamespace = 'public'::regnamespace +AND c.relowner = 'supabase_admin'::regrole; + +-- Reassign user objects in public schema from supabase_admin to postgres. +-- (Only affects public schema; Supabase-managed schemas stay as-is. +-- Extension-owned objects are skipped.) +DO $$ +DECLARE + rec record; + rel_count int := 0; + fn_count int := 0; + type_count int := 0; +BEGIN + -- Tables, views, sequences, materialized views, partitioned tables + FOR rec IN + SELECT c.relname, c.relkind + FROM pg_class c + WHERE c.relnamespace = 'public'::regnamespace + AND c.relowner = 'supabase_admin'::regrole + AND c.relkind IN ('r', 'v', 'S', 'm', 'p') + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_class'::regclass + AND d.objid = c.oid + AND d.deptype = 'e' + ) + ORDER BY CASE c.relkind + WHEN 'p' THEN 0 -- partitioned parents first; cascades ownership to partitions + WHEN 'm' THEN 1 + WHEN 'r' THEN 2 + WHEN 'v' THEN 3 + WHEN 'S' THEN 4 + END + LOOP + EXECUTE format('ALTER TABLE public.%I OWNER TO postgres', rec.relname); + rel_count := rel_count + 1; + END LOOP; + + -- Functions and procedures + FOR rec IN + SELECT p.oid, p.proname, pg_get_function_identity_arguments(p.oid) AS args + FROM pg_proc p + WHERE p.pronamespace = 'public'::regnamespace + AND p.proowner = 'supabase_admin'::regrole + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_proc'::regclass + AND d.objid = p.oid + AND d.deptype = 'e' + ) + LOOP + EXECUTE format('ALTER ROUTINE public.%I(%s) OWNER TO postgres', rec.proname, rec.args); + fn_count := fn_count + 1; + END LOOP; + + -- Types (excluding array types and table-bound composites) + FOR rec IN + SELECT t.typname + FROM pg_type t + WHERE t.typnamespace = 'public'::regnamespace + AND t.typowner = 'supabase_admin'::regrole + AND t.typrelid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_type el + WHERE el.oid = t.typelem + AND el.typarray = t.oid + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_type'::regclass + AND d.objid = t.oid + AND d.deptype = 'e' + ) + LOOP + EXECUTE format('ALTER TYPE public.%I OWNER TO postgres', rec.typname); + type_count := type_count + 1; + END LOOP; + + RAISE NOTICE 'Reassigned % relation(s), % routine(s), % type(s) from supabase_admin to postgres.', + rel_count, fn_count, type_count; +END +$$; +EOF + +echo "" +echo "Done." From 038c72cc6d34eeec9adeb05801085aea12cf1220 Mon Sep 17 00:00:00 2001 From: Timothy Lim Date: Wed, 22 Apr 2026 17:52:01 +0800 Subject: [PATCH 03/63] chore(docs): Minor formatting fixes (#45109) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Fixes minor formatting issue and removes extra unneeded quotes Screen Capture 2026-04-22 at 13 31
39@2x Screen Capture 2026-04-22 at 13 32
39@2x ## Summary by CodeRabbit * **Documentation** * Fixed formatting inconsistencies across troubleshooting articles by standardizing title metadata formatting and improving text readability in notices and tips sections. --- ...disk-size-not-shrinking-after-deleting-data-135390.mdx | 2 +- ...t-detailed-storage-metrics-with-the-aws-cli-587a7d.mdx | 2 +- ...s-are-not-visible-in-the-supabase-dashboard-4415aa.mdx | 2 +- ...res-token-has-expired-or-otp_expired-errors-5ee4d0.mdx | 2 +- .../rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx | 8 ++------ ...ovisioned-via-bolt-not-visible-in-dashboard-7188fc.mdx | 2 +- ...-operations-and-hierarchical-rls-challenges-b05a4d.mdx | 2 +- ...-provide-static-egress-ips-for-whitelisting-3d78b0.mdx | 2 +- 8 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/docs/content/troubleshooting/disk-size-not-shrinking-after-deleting-data-135390.mdx b/apps/docs/content/troubleshooting/disk-size-not-shrinking-after-deleting-data-135390.mdx index 741ecadb9a6..177b69ac079 100644 --- a/apps/docs/content/troubleshooting/disk-size-not-shrinking-after-deleting-data-135390.mdx +++ b/apps/docs/content/troubleshooting/disk-size-not-shrinking-after-deleting-data-135390.mdx @@ -1,5 +1,5 @@ --- -title = "'Disk size not shrinking after deleting data'" +title = "Disk size not shrinking after deleting data" topics = [ "database", "storage" ] keywords = [] --- diff --git a/apps/docs/content/troubleshooting/get-detailed-storage-metrics-with-the-aws-cli-587a7d.mdx b/apps/docs/content/troubleshooting/get-detailed-storage-metrics-with-the-aws-cli-587a7d.mdx index 3d053f7ff55..742b2d7adb3 100644 --- a/apps/docs/content/troubleshooting/get-detailed-storage-metrics-with-the-aws-cli-587a7d.mdx +++ b/apps/docs/content/troubleshooting/get-detailed-storage-metrics-with-the-aws-cli-587a7d.mdx @@ -1,5 +1,5 @@ --- -title = "'Get detailed Storage metrics with the AWS CLI'" +title = "Get detailed Storage metrics with the AWS CLI" topics = [ "cli", "storage", "studio" ] keywords = [] database_id = "2c33968e-26c0-4613-aee1-5de36d068394" diff --git a/apps/docs/content/troubleshooting/manually-created-databases-are-not-visible-in-the-supabase-dashboard-4415aa.mdx b/apps/docs/content/troubleshooting/manually-created-databases-are-not-visible-in-the-supabase-dashboard-4415aa.mdx index 3e11c06a837..079069d919d 100644 --- a/apps/docs/content/troubleshooting/manually-created-databases-are-not-visible-in-the-supabase-dashboard-4415aa.mdx +++ b/apps/docs/content/troubleshooting/manually-created-databases-are-not-visible-in-the-supabase-dashboard-4415aa.mdx @@ -1,5 +1,5 @@ --- -title = "'Manually created databases are not visible in the Supabase Dashboard'" +title = "Manually created databases are not visible in the Supabase Dashboard" topics = [ "database"] keywords = [] database_id = "f6420e72-ea67-4825-b3f7-e722ea5c0d96" diff --git a/apps/docs/content/troubleshooting/otp-verification-failures-token-has-expired-or-otp_expired-errors-5ee4d0.mdx b/apps/docs/content/troubleshooting/otp-verification-failures-token-has-expired-or-otp_expired-errors-5ee4d0.mdx index 98e8ca34353..7b36591ac5d 100644 --- a/apps/docs/content/troubleshooting/otp-verification-failures-token-has-expired-or-otp_expired-errors-5ee4d0.mdx +++ b/apps/docs/content/troubleshooting/otp-verification-failures-token-has-expired-or-otp_expired-errors-5ee4d0.mdx @@ -1,5 +1,5 @@ --- -title = "'OTP Verification Failures: 'token has expired' or 'otp_expired' errors'" +title = "OTP Verification Failures: 'token has expired' or 'otp_expired' errors" topics = [ "auth", "cli" ] keywords = [] database_id = "25eddb73-3cca-485b-b87f-7279dd46b7a7" diff --git a/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx b/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx index 5e0f696e63a..297ca4c355d 100644 --- a/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx +++ b/apps/docs/content/troubleshooting/rotating-anon-service-and-jwt-secrets-1Jq6yd.mdx @@ -9,17 +9,13 @@ database_id = "caa72a34-696c-47c6-8976-e50cf7fb396e" -This troubleshooting guide is about rotating **Legacy anon, service_role API keys**. We are deprecating Legacy -, and recommend migrating to New API keys. To learn more about API -keys, refer to [the API documentation](/docs/guides/api/api-keys). +This troubleshooting guide is about rotating **Legacy anon, service_role API keys**. We are deprecating Legacy, and recommend migrating to New API keys. To learn more about API keys, refer to [the API documentation](/docs/guides/api/api-keys). -Once the JWT secret is regenerated, all current API secrets will be immediately invalidated, and -all connections using them will be severed. You will need to deploy the new secrets for -connections to begin working again. You can avoid downtime by migrating to new API Keys. +Once the JWT secret is regenerated, all current API secrets will be immediately invalidated, and all connections using them will be severed. You will need to deploy the new secrets for connections to begin working again. You can avoid downtime by migrating to new API Keys. diff --git a/apps/docs/content/troubleshooting/supabase-project-provisioned-via-bolt-not-visible-in-dashboard-7188fc.mdx b/apps/docs/content/troubleshooting/supabase-project-provisioned-via-bolt-not-visible-in-dashboard-7188fc.mdx index e3b477a7d42..e0c25c701f0 100644 --- a/apps/docs/content/troubleshooting/supabase-project-provisioned-via-bolt-not-visible-in-dashboard-7188fc.mdx +++ b/apps/docs/content/troubleshooting/supabase-project-provisioned-via-bolt-not-visible-in-dashboard-7188fc.mdx @@ -1,5 +1,5 @@ --- -title = "'Supabase project provisioned via Bolt not visible in dashboard'" +title = "Supabase project provisioned via Bolt not visible in dashboard" topics = [ "database" ] keywords = ["Bolt"] database_id = "d64f5f5d-ef80-4423-ba24-1a79b4e69ce6" diff --git a/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx b/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx index 4f6b50024ed..00c22e3cf19 100644 --- a/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx +++ b/apps/docs/content/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d.mdx @@ -1,5 +1,5 @@ --- -title = "'Supabase Storage: Inefficient folder operations and hierarchical RLS challenges'" +title = "Supabase Storage: Inefficient folder operations and hierarchical RLS challenges" topics = [ "storage" ] keywords = [] database_id = "3b52daf2-d78d-4630-8e9f-8bf5d90208bf" diff --git a/apps/docs/content/troubleshooting/why-supabase-edge-functions-cannot-provide-static-egress-ips-for-whitelisting-3d78b0.mdx b/apps/docs/content/troubleshooting/why-supabase-edge-functions-cannot-provide-static-egress-ips-for-whitelisting-3d78b0.mdx index 6526e5892f0..4bd44fe92ca 100644 --- a/apps/docs/content/troubleshooting/why-supabase-edge-functions-cannot-provide-static-egress-ips-for-whitelisting-3d78b0.mdx +++ b/apps/docs/content/troubleshooting/why-supabase-edge-functions-cannot-provide-static-egress-ips-for-whitelisting-3d78b0.mdx @@ -1,5 +1,5 @@ --- -title = "'Why Supabase Edge Functions cannot provide static egress IPs for allow listing'" +title = "Why Supabase Edge Functions cannot provide static egress IPs for allow listing" topics = [ "auth", "functions", "platform", "self-hosting" ] keywords = [] database_id = "99b56e92-62f2-4602-8e13-d38caf7aff4c" From 2371bb02905cc6198cd4dbe8dccaa0849d4a58eb Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:24:27 +0530 Subject: [PATCH 04/63] fix(docs): smtp access (#45094) fixes the SMTP settings permission in the Access Control docs image ## ref: - closes https://github.com/supabase/supabase/issues/45088 ## Summary by CodeRabbit ## Documentation * Improved table formatting in access control documentation for better visual consistency and clarity. --- apps/docs/content/guides/platform/access-control.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/access-control.mdx b/apps/docs/content/guides/platform/access-control.mdx index 5a8dca998dd..e3490f0d70c 100644 --- a/apps/docs/content/guides/platform/access-control.mdx +++ b/apps/docs/content/guides/platform/access-control.mdx @@ -187,7 +187,7 @@ The table below shows the actions each role can take on the resources belonging | Auth Settings | View | | | | | | | Update | | | | | | SMTP Settings | View | | | | | -| | Update | | | | | +| | Update | | | | | | Advanced Settings | View | | | | | | | Update | | | | | | **Storage Configuration** | | | | | | From 7a3292c127238d1f2e5e85dd84069df1cd59572c Mon Sep 17 00:00:00 2001 From: Taryn King <49492414+tk1ng@users.noreply.github.com> Date: Wed, 22 Apr 2026 04:58:17 -0500 Subject: [PATCH 05/63] Update language around legacy key rotation (#45104) --- apps/docs/content/guides/api/api-keys.mdx | 2 -- apps/docs/content/guides/auth/signing-keys.mdx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/docs/content/guides/api/api-keys.mdx b/apps/docs/content/guides/api/api-keys.mdx index 5b488ec1190..f1a7ec258c5 100644 --- a/apps/docs/content/guides/api/api-keys.mdx +++ b/apps/docs/content/guides/api/api-keys.mdx @@ -150,8 +150,6 @@ Rotating a secret key (`sb_secret_...`) is easy and painless. Use the [API Keys] If you are still using the JWT-based `service_role` key, replace the `service_role` key with a new secret key instead. Follow the guide from above as if you are rotating an existing secret key. -If you believe this is not possible for your implementation, [contact Support](/dashboard/support/new). - ## Known limitations and compatibility differences As the publishable and secret keys are no longer JWT-based, there are some known limitations and compatibility differences that you may need to plan for: diff --git a/apps/docs/content/guides/auth/signing-keys.mdx b/apps/docs/content/guides/auth/signing-keys.mdx index 2b0ad710e2b..9ca24c1656b 100644 --- a/apps/docs/content/guides/auth/signing-keys.mdx +++ b/apps/docs/content/guides/auth/signing-keys.mdx @@ -36,7 +36,7 @@ We've designed the Signing keys system to address many problems the legacy syste You can start migrating away from the legacy JWT secret through the Supabase dashboard. This process does not cause downtime for your application. -1. Start off by clicking the _Migrate JWT secret_ button on the [JWT signing keys](/dashboard/project/_/settings/jwt) page. This step will import the existing legacy JWT secret into the new JWT signing keys system. Once this process completes, you will no longer be able to rotate the legacy JWT secret using the old system. +1. Start off by clicking the _Migrate JWT secret_ button on the [JWT signing keys](/dashboard/project/_/settings/jwt) page. This step will import the existing legacy JWT secret into the new JWT signing keys system. 2. Simultaneously, we're creating a new asymmetric JWT signing key for you to rotate to. This key starts off as standby key -- meaning it's being advertised as a key that Supabase Auth will use in the future to create JWTs. 3. If you're not ready to switch away from the legacy JWT secret right now, you can stop here without any issue. If you wish to use a different signing key -- either to use a different signing algorithm (RSA, Elliptic Curve or shared secret) or to import a private key or shared secret you already have -- feel free to move the standby key to _Previously used_ before finally moving it to _Revoked._ 4. If you do wish to start using the standby key for all new JWT use the _Rotate keys_ button. A few important notes: From dbc68d9e2cd5e49a449f64d96f57c8a9e8fd1da7 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe <276348853+colings86-work@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:46:12 +0100 Subject: [PATCH 06/63] Update humans.txt - Add Colin Goodheart-Smithe (#45059) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Updates humans.txt to include my name. ## What is the current behavior? N/A ## What is the new behavior? N/A ## Additional context N/A ## Summary by CodeRabbit * **Chores** * Updated the contributor list. --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index c9185a8c8d6..4fbdcc025ab 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -52,6 +52,7 @@ Chris Martin Chris Stockton Chris Ward Clayton Kast +Colin Goodheart-Smithe Colin Murray Colum Ferry Craig Cannon From 3b4ec65bfb7aa41e3d46203f046a23250f299885 Mon Sep 17 00:00:00 2001 From: Jonathan <82057176+mgalore@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:51:54 +0000 Subject: [PATCH 07/63] docs: fix broken Vecs source link in DEVELOPERS guide (#45107) ## Summary - fix the Markdown link to the Vecs Python source example in `DEVELOPERS.md` ## Why The current link is missing a closing parenthesis, which breaks the link target in the federated docs section. This restores the intended reference for contributors working with external docs sources. ## Testing - docs-only change AI-assisted: yes ## Summary by CodeRabbit * **Documentation** * Fixed a formatting issue in developer documentation. Co-authored-by: Jonathan Amponsah --- DEVELOPERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOPERS.md b/DEVELOPERS.md index d228ea7b169..6bf8f3c41db 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -192,7 +192,7 @@ We support "federating" docs, meaning doc content can come directly from externa Federated docs work using Next.js's build pipeline. We use `getStaticProps()` to fetch remote documentation (ie. markdown) at build time which is processed and passed to the respective page within the docs. -See the [Vecs Python source code](https://github.com/supabase/supabase/tree/master/apps/docs/app/guides/ai/python/%5Bslug%5D to see how we do this for [`supabase/vecs`](https://github.com/supabase/vecs). Use this as a starting point for federating other docs. +See the [Vecs Python source code](https://github.com/supabase/supabase/tree/master/apps/docs/app/guides/ai/python/%5Bslug%5D) to see how we do this for [`supabase/vecs`](https://github.com/supabase/vecs). Use this as a starting point for federating other docs. Some things to consider: From 64d667eeff2e072d7b5f4cb6d1327d41feb46853 Mon Sep 17 00:00:00 2001 From: Chandana Anumula <129955975+canumula@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:25:03 +0530 Subject: [PATCH 08/63] Update quickstart.mdx (#45115) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update ## Summary by CodeRabbit * **Documentation** * Added a curl command example in the API quickstart guide, demonstrating how to query the todos endpoint with proper authentication headers for quick API testing. --- apps/docs/content/guides/api/quickstart.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/docs/content/guides/api/quickstart.mdx b/apps/docs/content/guides/api/quickstart.mdx index d3050706082..511222e3a98 100644 --- a/apps/docs/content/guides/api/quickstart.mdx +++ b/apps/docs/content/guides/api/quickstart.mdx @@ -200,6 +200,14 @@ You can query the route in your browser, by appending the `publishable` key as a `https://.supabase.co/rest/v1/todos?apikey=` +### Curl + +``` +curl 'https://.supabase.co/rest/v1/todos?select=*' \ + -H "apikey: " \ + -H "Authorization: Bearer " +``` + ### Client libraries We provide a number of [Client Libraries](https://github.com/supabase/supabase#client-libraries). From 061c213d6508981ba751cd58391267a47f4373fd Mon Sep 17 00:00:00 2001 From: Eduardo Gurgel Date: Wed, 22 Apr 2026 23:03:23 +1200 Subject: [PATCH 09/63] chore(docs): remove beta/alpha notices (#45105) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YE ## What kind of change does this PR introduce? ## Summary by CodeRabbit * **Documentation** * Removed public beta status notice from Realtime Authorization guide * Removed public beta status notice from the Broadcast from the Database section * Removed public alpha status notice from the Broadcast replay section Co-authored-by: Chris Chinchilla --- apps/docs/content/guides/realtime/authorization.mdx | 6 ------ apps/docs/content/guides/realtime/broadcast.mdx | 12 ------------ 2 files changed, 18 deletions(-) diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx index 26ad20d965d..62065ac1ceb 100644 --- a/apps/docs/content/guides/realtime/authorization.mdx +++ b/apps/docs/content/guides/realtime/authorization.mdx @@ -12,12 +12,6 @@ You can control client access to Realtime [Broadcast](/docs/guides/realtime/broa - Control which clients can publish their presence to a Channel - Control which clients can receive messages about the presence of other clients - - -Realtime Authorization is in Public Beta. To use Authorization for your Realtime Channels, use `supabase-js` version `v2.44.0` or later. - - - To enforce private channels you need to disable the 'Allow public access' setting in [Realtime Settings](/dashboard/project/_/realtime/settings) diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index aec5b0189dc..e047672f7c2 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -421,12 +421,6 @@ You can use the Supabase client libraries to send Broadcast messages. ### Broadcast from the Database - - -This feature is in Public Beta. [Submit a support ticket](https://supabase.help) if you have any issues. - - - All the messages sent using Broadcast from the Database are stored in `realtime.messages` table and will be deleted after 3 days. @@ -988,12 +982,6 @@ const changes = supabase ## Broadcast replay - - -This feature is currently in Public Alpha. If you have any issues [submit a support ticket](https://supabase.help). - - - ### How it works Broadcast Replay enables **private** channels to access messages that were sent earlier. Only messages published via [Broadcast From the Database](#broadcast-from-the-database) are available for replay. From 5d5ac18f3c38006ac018203be10d8419be18fb1a Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:18:38 +0200 Subject: [PATCH 10/63] Fix cron job form does not focus invalid inputs (#45114) ## Problem When trying to create a new CRON job with an already used name, the `name` input isn't focused. As a result, users don't see the error. ## Solution Fix the call to `form.setError` so that it focuses the input ## Summary by CodeRabbit ## Release Notes * **Bug Fixes** * Improved error handling in cron job creation. When a duplicate job name is detected, the form now automatically focuses on the name field, providing clearer feedback to help you quickly identify and correct validation errors. --- .../CreateCronJobSheet/CreateCronJobSheet.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx index 16228995cce..1ff314b81d7 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet/CreateCronJobSheet.tsx @@ -176,10 +176,14 @@ export const CreateCronJobSheet = ({ open, selectedCronJob, onClose }: CreateCro const nameExists = !!checkExistingJob if (nameExists) { - return form.setError('name', { - type: 'manual', - message: 'A cron job with this name already exists', - }) + return form.setError( + 'name', + { + type: 'manual', + message: 'A cron job with this name already exists', + }, + { shouldFocus: true } + ) } } catch (error: any) { toast.error(`Failed to validate cron job name: ${error.message}`) From 40791f9846253437f91875b5ede53afabc15b6d9 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Wed, 22 Apr 2026 06:19:32 -0600 Subject: [PATCH 11/63] chore(studio): migrate useHotKey to useShortcut (#45099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Migrates all 11 `useHotKey` call sites across 9 files to `useShortcut`, backed by `SHORTCUT_DEFINITIONS` in `state/shortcuts/registry.ts`. - Adds 10 new registry entries (all `showInSettings: false` to keep behavior identical to today — these were not previously user-configurable). - Deletes `apps/studio/hooks/ui/useHotKey.ts`. - Simplifies `ActionBar.handleSave` — the legacy hook passed a `KeyboardEvent` the callback used for `preventDefault`/`stopPropagation` and a textarea-plain-Enter guard; all of that is redundant under `useShortcut` (TanStack handles default/propagation; `Mod+Enter` never fires on plain Enter). - Removes a stale commented-out `useHotKey` reference in `DataTableFilterCommand.tsx`. Part of FE-3025 (legacy hotkey hook cleanup). `useKeyboardShortcuts` in `grid/components/common/Hooks.tsx` will be migrated in a follow-up. ## Test plan All shortcuts should still fire with **Cmd** (macOS) / **Ctrl** (Win/Linux). **Table Editor — operation queue** (requires pending unsaved edits on a row) - [x] `Cmd+S` saves pending edits - [x] `Cmd+.` toggles the operation queue side panel - [x] `Cmd+Z` undoes the latest edit and re-fetches the affected table rows - [x] With no pending edits, none of the above fire (gated by `isEnabled`) **Table Editor — side panel editor forms** (row, table, column, policy, etc.) - [x] `Cmd+Enter` submits the form when the panel is visible - [x] Does not submit if the form is disabled/loading or the panel is hidden **Unified Logs — data table** - [x] `Cmd+B` toggles the filter controls sidebar (desktop) - [x] `Cmd+B` opens the filter drawer (mobile, ` ## Summary by CodeRabbit ## Refactor * Implemented a centralized keyboard shortcut registry system for managing shortcuts consistently across the application * Updated multiple UI components throughout the interface to use the new shortcut management system * All existing keyboard shortcuts continue to function without any changes in behavior or user experience ## Chores * Removed legacy keyboard shortcut hook implementation --- .../grid/hooks/useOperationQueueShortcuts.ts | 29 +++----- .../SidePanelEditor/ActionBar.tsx | 41 ++++------- .../UnifiedLogs/UnifiedLogs.hooks.ts | 7 +- .../ui/AIAssistantPanel/AIAssistant.tsx | 5 +- .../DataTableFilterCommand.tsx | 3 - .../DataTableFilterControlsDrawer.tsx | 7 +- .../ui/DataTable/DataTableInfinite.tsx | 7 +- .../ui/DataTable/DataTableResetButton.tsx | 5 +- .../ui/DataTable/DataTableToolbar.tsx | 5 +- .../components/ui/DataTable/LiveButton.tsx | 5 +- apps/studio/hooks/ui/useHotKey.ts | 66 ----------------- apps/studio/state/shortcuts/registry.ts | 70 +++++++++++++++++++ 12 files changed, 118 insertions(+), 132 deletions(-) delete mode 100644 apps/studio/hooks/ui/useHotKey.ts diff --git a/apps/studio/components/grid/hooks/useOperationQueueShortcuts.ts b/apps/studio/components/grid/hooks/useOperationQueueShortcuts.ts index 0ea7a3384d6..bbde506e96c 100644 --- a/apps/studio/components/grid/hooks/useOperationQueueShortcuts.ts +++ b/apps/studio/components/grid/hooks/useOperationQueueShortcuts.ts @@ -4,7 +4,8 @@ import { useOperationQueueActions } from './useOperationQueueActions' import { useIsQueueOperationsEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings' import { tableRowKeys } from '@/data/table-rows/keys' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' import { useTableEditorStateSnapshot } from '@/state/table-editor' /** @@ -28,44 +29,36 @@ export function useOperationQueueShortcuts() { const hasOperations = snap.hasPendingOperations const isEnabled = isQueueOperationsEnabled && hasOperations - useHotKey( - (event) => { - event.preventDefault() - event.stopPropagation() + useShortcut( + SHORTCUT_IDS.OPERATION_QUEUE_SAVE, + () => { if (!isSaving && hasOperations) { handleSave() } }, - 's', { enabled: isEnabled } ) - useHotKey( - (event) => { - event.preventDefault() - event.stopPropagation() + useShortcut( + SHORTCUT_IDS.OPERATION_QUEUE_TOGGLE, + () => { snap.toggleViewOperationQueue() }, - '.', { enabled: isEnabled } ) - useHotKey( - (event) => { - event.preventDefault() - event.stopPropagation() - + useShortcut( + SHORTCUT_IDS.OPERATION_QUEUE_UNDO, + () => { const tableIdLatestOperation = snap.operationQueue.operations.at(-1)?.tableId snap.undoLatestOperation() - // Invalidate the query to revert the optimistic update if (project && tableIdLatestOperation) { queryClient.invalidateQueries({ queryKey: tableRowKeys.tableRowsAndCount(project.ref, tableIdLatestOperation), }) } }, - 'z', { enabled: isEnabled } ) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx index b50ed0d54c0..43b26fd838b 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx @@ -2,7 +2,8 @@ import { noop } from 'lodash' import { PropsWithChildren, useCallback, useState } from 'react' import { Button, KeyboardShortcut } from 'ui' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' interface ActionBarProps { loading?: boolean @@ -37,36 +38,20 @@ export const ActionBar = ({ setIsRunning(false) }, [applyFunction]) - const handleSave = useCallback( - (event: KeyboardEvent) => { - // Don't trigger if already running/loading, or if apply is disabled/hidden - if (isRunning || loading || disableApply || hideApply) return + const handleSave = useCallback(() => { + if (isRunning || loading || disableApply || hideApply) return - // Don't trigger if the user is in a textarea (allow multi-line entry) - // unless they explicitly press CMD+Enter - const activeElement = document.activeElement - const isTextarea = activeElement?.tagName === 'TEXTAREA' - - // If in a textarea and this is just an Enter key (not CMD+Enter), don't submit - if (isTextarea && !event.metaKey && !event.ctrlKey) return - - event.preventDefault() - event.stopPropagation() - - if (formId) { - // Form-based submission - programmatically submit the form - const form = document.getElementById(formId) as HTMLFormElement | null - if (form) { - form.requestSubmit() - } - } else if (applyFunction) { - onSelectApply() + if (formId) { + const form = document.getElementById(formId) as HTMLFormElement | null + if (form) { + form.requestSubmit() } - }, - [isRunning, loading, disableApply, hideApply, formId, applyFunction, onSelectApply] - ) + } else if (applyFunction) { + onSelectApply() + } + }, [isRunning, loading, disableApply, hideApply, formId, applyFunction, onSelectApply]) - useHotKey(handleSave, 'Enter', { enabled: visible }) + useShortcut(SHORTCUT_IDS.ACTION_BAR_SAVE, handleSave, { enabled: visible }) return (
diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts index c8ef733224c..af67d64f4f0 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.hooks.ts @@ -2,17 +2,18 @@ import { useQueryState } from 'nuqs' import { useEffect, useMemo, useRef } from 'react' import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' export const useResetFocus = () => { - useHotKey(() => { + useShortcut(SHORTCUT_IDS.UNIFIED_LOGS_RESET_FOCUS, () => { // FIXME: some dedicated div[tabindex="0"] do not auto-unblur (e.g. the DataTableFilterResetButton) // REMINDER: we cannot just document.activeElement?.blur(); as the next tab will focus the next element in line, // which is not what we want. We want to reset entirely. document.body.setAttribute('tabindex', '0') document.body.focus() document.body.removeAttribute('tabindex') - }, '.') + }) } export const useLiveMode = (data: TData[]) => { diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 6c3dabef41c..04b994a3fcd 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -35,7 +35,6 @@ import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useOrgAiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { useHotKey } from '@/hooks/ui/useHotKey' import { DEFAULT_ASSISTANT_BASE_MODEL_ID, defaultAssistantModelId, @@ -47,6 +46,8 @@ import { uuidv4 } from '@/lib/helpers' import { useTrack } from '@/lib/telemetry/track' import type { AssistantModel } from '@/state/ai-assistant-state' import { useAiAssistantState, useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2' @@ -64,7 +65,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { const { data: selectedOrganization, isPending: isLoadingOrganization } = useSelectedOrganizationQuery() - useHotKey(() => cancelEdit(), 'Escape') + useShortcut(SHORTCUT_IDS.AI_ASSISTANT_CANCEL_EDIT, () => cancelEdit()) const disablePrompts = useFlag('disableAssistantPrompts') const { snippets } = useSqlEditorV2StateSnapshot() diff --git a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCommand.tsx b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCommand.tsx index 16d87c701e7..4fa114167bf 100644 --- a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCommand.tsx +++ b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCommand.tsx @@ -66,9 +66,6 @@ export function DataTableFilterCommand({ (x) => typeof x.value === 'string' && currentWord.includes(`${x.value}:`) ) - // [Joshen] Temporarily disabling as this conflicts with our current CMD K behaviour - // useHotKey(() => setOpen((open) => !open), 'k') - useEffect(() => { // TODO: we could check for ARRAY_DELIMITER or SLIDER_DELIMITER to auto-set filter when typing if (currentWord !== '' && open) return diff --git a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterControlsDrawer.tsx b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterControlsDrawer.tsx index ec2ae977248..90ba86a9119 100644 --- a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterControlsDrawer.tsx +++ b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterControlsDrawer.tsx @@ -19,15 +19,16 @@ import { import { useMediaQuery } from '../hooks/useMediaQuery' import { Kbd } from '../primitives/Kbd' import { DataTableFilterControls } from './DataTableFilterControls' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' export function DataTableFilterControlsDrawer() { const triggerButtonRef = useRef(null) const isMobile = useMediaQuery('(max-width: 640px)') - useHotKey(() => { + useShortcut(SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS, () => { triggerButtonRef.current?.click() - }, 'b') + }) return ( diff --git a/apps/studio/components/ui/DataTable/DataTableInfinite.tsx b/apps/studio/components/ui/DataTable/DataTableInfinite.tsx index 5da20eb0918..824cdfc4aa3 100644 --- a/apps/studio/components/ui/DataTable/DataTableInfinite.tsx +++ b/apps/studio/components/ui/DataTable/DataTableInfinite.tsx @@ -9,7 +9,8 @@ import { Button, cn } from 'ui' import { formatCompactNumber } from './DataTable.utils' import { useDataTable } from './providers/DataTableProvider' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './Table' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' // TODO: add a possible chartGroupBy export interface DataTableInfiniteProps { @@ -64,10 +65,10 @@ export function DataTableInfinite({ [fetchNextPage, isFetching, totalRows, totalRowsFetched] ) - useHotKey(() => { + useShortcut(SHORTCUT_IDS.DATA_TABLE_RESET_COLUMNS, () => { setColumnOrder([]) setColumnVisibility(defaultColumnVisibility) - }, 'u') + }) return ( diff --git a/apps/studio/components/ui/DataTable/DataTableResetButton.tsx b/apps/studio/components/ui/DataTable/DataTableResetButton.tsx index a2a2b379e1d..17ca6c33af1 100644 --- a/apps/studio/components/ui/DataTable/DataTableResetButton.tsx +++ b/apps/studio/components/ui/DataTable/DataTableResetButton.tsx @@ -3,11 +3,12 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { Kbd } from './primitives/Kbd' import { useDataTable } from './providers/DataTableProvider' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' export function DataTableResetButton() { const { table } = useDataTable() - useHotKey(() => table.resetColumnFilters(), 'Escape') + useShortcut(SHORTCUT_IDS.DATA_TABLE_RESET_FILTERS, () => table.resetColumnFilters()) return ( diff --git a/apps/studio/components/ui/DataTable/DataTableToolbar.tsx b/apps/studio/components/ui/DataTable/DataTableToolbar.tsx index e61f15f4722..8862e008ded 100644 --- a/apps/studio/components/ui/DataTable/DataTableToolbar.tsx +++ b/apps/studio/components/ui/DataTable/DataTableToolbar.tsx @@ -8,7 +8,8 @@ import { DataTableResetButton } from './DataTableResetButton' import { DataTableViewOptions } from './DataTableViewOptions' import { Kbd } from './primitives/Kbd' import { useDataTable } from './providers/DataTableProvider' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' interface DataTableToolbarProps { renderActions?: () => ReactNode @@ -24,7 +25,7 @@ export function DataTableToolbar({ const { table, isLoading, columnFilters } = useDataTable() const filters = table.getState().columnFilters - useHotKey(() => setIsFilterBarOpen((prev) => !prev), 'b') + useShortcut(SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS, () => setIsFilterBarOpen((prev) => !prev)) const rows = useMemo( () => ({ diff --git a/apps/studio/components/ui/DataTable/LiveButton.tsx b/apps/studio/components/ui/DataTable/LiveButton.tsx index e361c591b53..247a379a319 100644 --- a/apps/studio/components/ui/DataTable/LiveButton.tsx +++ b/apps/studio/components/ui/DataTable/LiveButton.tsx @@ -5,7 +5,8 @@ import { useEffect } from 'react' import { Button, cn } from 'ui' import { useDataTable } from './providers/DataTableProvider' -import { useHotKey } from '@/hooks/ui/useHotKey' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' const REFRESH_INTERVAL = 10_000 @@ -17,7 +18,7 @@ interface LiveButtonProps { export function LiveButton({ fetchPreviousPage, searchParamsParser }: LiveButtonProps) { const [{ live, date, sort }, setSearch] = useQueryStates(searchParamsParser) const { table } = useDataTable() - useHotKey(handleClick, 'j') + useShortcut(SHORTCUT_IDS.DATA_TABLE_TOGGLE_LIVE, handleClick) useEffect(() => { let timeoutId: NodeJS.Timeout diff --git a/apps/studio/hooks/ui/useHotKey.ts b/apps/studio/hooks/ui/useHotKey.ts deleted file mode 100644 index cedb801e180..00000000000 --- a/apps/studio/hooks/ui/useHotKey.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect } from 'react' -import { useLatest } from 'react-use' - -/** - * @deprecated Use `useShortcut` from `state/shortcuts/useShortcut` instead. - * It reads from a central shortcut registry (`SHORTCUT_DEFINITIONS`) and - * integrates with the user's enable/disable preferences + the Cmd+P command menu. - * - * Migration: - * 1. Add an entry to `state/shortcuts/registry.ts` with a unique ID, label, and sequence. - * 2. Replace `useHotKey(cb, 'k', { shift: true })` with - * `useShortcut(SHORTCUT_IDS.YOUR_ID, cb)`. - */ -function useHotKey( - callback: (e: KeyboardEvent) => void, - key: string, - options?: { enabled?: boolean; shift?: boolean } -): void -/** - * @deprecated Use `useShortcut` from `state/shortcuts/useShortcut` instead. - * The `dependencies` parameter is also deprecated in this legacy hook. - */ -function useHotKey( - callback: (e: KeyboardEvent) => void, - key: string, - dependencies: unknown[], - options?: { enabled?: boolean; shift?: boolean } -): void -function useHotKey( - callback: (e: KeyboardEvent) => void, - key: string, - dependenciesOrOptions?: unknown[] | { enabled?: boolean; shift?: boolean }, - options?: { enabled?: boolean; shift?: boolean } -): void { - // Determine which overload was called - const isDepsArray = Array.isArray(dependenciesOrOptions) - const resolvedOptions = isDepsArray ? options : dependenciesOrOptions - const enabled = resolvedOptions?.enabled ?? true - const shift = resolvedOptions?.shift ?? false - - const enabledRef = useLatest(enabled) - const callbackRef = useLatest(callback) - const keyRef = useLatest(key) - const shiftRef = useLatest(shift) - - useEffect(() => { - function handler(e: KeyboardEvent) { - if (!enabledRef.current) return - if ( - (e.metaKey || e.ctrlKey) && - e.key.toLowerCase() === keyRef.current.toLowerCase() && - !e.altKey && - (shiftRef.current ? e.shiftKey : !e.shiftKey) - ) { - callbackRef.current(e) - } - } - - window.addEventListener('keydown', handler, true) - return () => { - window.removeEventListener('keydown', handler, true) - } - }, [callbackRef, enabledRef, keyRef, shiftRef]) -} - -export { useHotKey } diff --git a/apps/studio/state/shortcuts/registry.ts b/apps/studio/state/shortcuts/registry.ts index 542c01c5117..3d33e4dd102 100644 --- a/apps/studio/state/shortcuts/registry.ts +++ b/apps/studio/state/shortcuts/registry.ts @@ -10,11 +10,21 @@ import { ShortcutDefinition } from './types' export const SHORTCUT_IDS = { COMMAND_MENU_OPEN: 'command-menu.open', AI_ASSISTANT_TOGGLE: 'ai-assistant.toggle', + AI_ASSISTANT_CANCEL_EDIT: 'ai-assistant.cancel-edit', INLINE_EDITOR_TOGGLE: 'inline-editor.toggle', RESULTS_COPY_MARKDOWN: 'results.copy-markdown', RESULTS_COPY_JSON: 'results.copy-json', RESULTS_COPY_CSV: 'results.copy-csv', RESULTS_DOWNLOAD_CSV: 'results.download-csv', + DATA_TABLE_TOGGLE_FILTERS: 'data-table.toggle-filters', + DATA_TABLE_RESET_FILTERS: 'data-table.reset-filters', + DATA_TABLE_RESET_COLUMNS: 'data-table.reset-columns', + DATA_TABLE_TOGGLE_LIVE: 'data-table.toggle-live', + ACTION_BAR_SAVE: 'action-bar.save', + OPERATION_QUEUE_SAVE: 'operation-queue.save', + OPERATION_QUEUE_TOGGLE: 'operation-queue.toggle', + OPERATION_QUEUE_UNDO: 'operation-queue.undo', + UNIFIED_LOGS_RESET_FOCUS: 'unified-logs.reset-focus', } as const /** @@ -79,4 +89,64 @@ export const SHORTCUT_DEFINITIONS: Record = { label: 'Download results as CSV', sequence: ['Mod+Shift+D'], }, + [SHORTCUT_IDS.AI_ASSISTANT_CANCEL_EDIT]: { + id: SHORTCUT_IDS.AI_ASSISTANT_CANCEL_EDIT, + label: 'Cancel AI Assistant edit', + sequence: ['Mod+Escape'], + showInSettings: false, + }, + [SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS]: { + id: SHORTCUT_IDS.DATA_TABLE_TOGGLE_FILTERS, + label: 'Toggle data table filter controls', + sequence: ['Mod+B'], + showInSettings: false, + }, + [SHORTCUT_IDS.DATA_TABLE_RESET_FILTERS]: { + id: SHORTCUT_IDS.DATA_TABLE_RESET_FILTERS, + label: 'Reset data table filters', + sequence: ['Mod+Escape'], + showInSettings: false, + }, + [SHORTCUT_IDS.DATA_TABLE_RESET_COLUMNS]: { + id: SHORTCUT_IDS.DATA_TABLE_RESET_COLUMNS, + label: 'Reset data table columns', + sequence: ['Mod+U'], + showInSettings: false, + }, + [SHORTCUT_IDS.DATA_TABLE_TOGGLE_LIVE]: { + id: SHORTCUT_IDS.DATA_TABLE_TOGGLE_LIVE, + label: 'Toggle live mode', + sequence: ['Mod+J'], + showInSettings: false, + }, + [SHORTCUT_IDS.ACTION_BAR_SAVE]: { + id: SHORTCUT_IDS.ACTION_BAR_SAVE, + label: 'Save form', + sequence: ['Mod+Enter'], + showInSettings: false, + }, + [SHORTCUT_IDS.OPERATION_QUEUE_SAVE]: { + id: SHORTCUT_IDS.OPERATION_QUEUE_SAVE, + label: 'Save pending table edits', + sequence: ['Mod+S'], + showInSettings: false, + }, + [SHORTCUT_IDS.OPERATION_QUEUE_TOGGLE]: { + id: SHORTCUT_IDS.OPERATION_QUEUE_TOGGLE, + label: 'Toggle operation queue panel', + sequence: ['Mod+.'], + showInSettings: false, + }, + [SHORTCUT_IDS.OPERATION_QUEUE_UNDO]: { + id: SHORTCUT_IDS.OPERATION_QUEUE_UNDO, + label: 'Undo latest table edit', + sequence: ['Mod+Z'], + showInSettings: false, + }, + [SHORTCUT_IDS.UNIFIED_LOGS_RESET_FOCUS]: { + id: SHORTCUT_IDS.UNIFIED_LOGS_RESET_FOCUS, + label: 'Reset focus in logs', + sequence: ['Mod+.'], + showInSettings: false, + }, } From 8b642db434ba6b0feaf948f6b374bf76e08dd76e Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Wed, 22 Apr 2026 14:25:26 +0200 Subject: [PATCH 12/63] chore: Bump vulnerable packages (#45112) ## Summary by CodeRabbit * **Chores** * Updated internal dependencies to maintain compatibility and stability. --- apps/ui-library/package.json | 2 +- pnpm-lock.yaml | 396 +++++------------------------------ 2 files changed, 55 insertions(+), 343 deletions(-) diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index 685a1739772..33a2e5d79a7 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -109,7 +109,7 @@ "mdast-util-toc": "^6.1.1", "postcss": "^8.5.3", "react-dropzone": "^14.3.8", - "react-router": "^7.12.0", + "react-router": "^7.13.2", "rimraf": "^4.1.3", "shadcn": "^3.0.0", "shiki": "^1.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06aa5fe636a..dca6197cf97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -580,7 +580,7 @@ importers: version: 0.3.2 '@testing-library/react': specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 @@ -1219,16 +1219,16 @@ importers: version: 0.1.1(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@6.0.2))) '@testing-library/dom': specifier: ^10.0.0 - version: 10.1.0 + version: 10.4.1 '@testing-library/jest-dom': specifier: ^6.6.0 version: 6.6.3 '@testing-library/react': specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': specifier: ^14.0.0 - version: 14.6.1(@testing-library/dom@10.1.0) + version: 14.6.1(@testing-library/dom@10.4.1) '@types/common-tags': specifier: ^1.8.1 version: 1.8.4 @@ -1465,7 +1465,7 @@ importers: version: 1.2.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-router/fs-routes': specifier: ^7.4.0 - version: 7.4.0(@react-router/dev@7.9.6(@react-router/serve@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3))(typescript@6.0.2) + version: 7.4.0(@react-router/dev@7.13.2(@react-router/serve@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3))(typescript@6.0.2) '@supabase-labs/y-supabase': specifier: 0.1.0 version: 0.1.0 @@ -1604,7 +1604,7 @@ importers: version: 7.29.0(supports-color@8.1.1) '@react-router/dev': specifier: ^7.9.0 - version: 7.9.6(@react-router/serve@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3) + version: 7.13.2(@react-router/serve@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3) '@shikijs/compat': specifier: ^1.1.7 version: 1.6.0 @@ -1648,8 +1648,8 @@ importers: specifier: ^14.3.8 version: 14.3.8(react@18.3.1) react-router: - specifier: ^7.12.0 - version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.13.2 + version: 7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rimraf: specifier: ^4.1.3 version: 4.4.1 @@ -2280,10 +2280,10 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': specifier: ^14.0.0 - version: 14.6.1(@testing-library/dom@10.4.0) + version: 14.6.1(@testing-library/dom@10.4.1) '@types/react': specifier: 'catalog:' version: 18.3.3 @@ -2611,7 +2611,7 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/lodash': specifier: 4.17.5 version: 4.17.5 @@ -2807,16 +2807,16 @@ importers: devDependencies: '@testing-library/dom': specifier: ^10.0.0 - version: 10.1.0 + version: 10.4.1 '@testing-library/jest-dom': specifier: ^6.6.0 version: 6.6.3 '@testing-library/react': specifier: ^16.0.0 - version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@testing-library/user-event': specifier: ^14.0.0 - version: 14.6.1(@testing-library/dom@10.1.0) + version: 14.6.1(@testing-library/dom@10.4.1) '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 @@ -3222,12 +3222,6 @@ packages: resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.28.3': - resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-create-class-features-plugin@7.28.6': resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} engines: {node: '>=6.9.0'} @@ -5065,18 +5059,6 @@ packages: resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - '@npmcli/git@4.1.0': - resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@npmcli/package-json@4.0.1': - resolution: {integrity: sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@npmcli/promise-spawn@6.0.2': - resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - '@number-flow/react@0.3.2': resolution: {integrity: sha512-/Rg7WjIZR/yjHJAzRHN7+Cif+s9U02QewMl9WEKPoAY9O6jg0wA/IsAl3lJgeM1ic31bDJ92wfCkwE9ud62VmQ==} peerDependencies: @@ -7839,27 +7821,6 @@ packages: wrangler: optional: true - '@react-router/dev@7.9.6': - resolution: {integrity: sha512-pBkbczGwI+NcZPcK8JPvWGWdjUpT/+okXYp6IXvt7zI3WLxr5hQLLRox5FkLiVxkykbqARO1hk9NRp9KFwJ2sA==} - engines: {node: '>=20.0.0'} - hasBin: true - peerDependencies: - '@react-router/serve': ^7.9.6 - '@vitejs/plugin-rsc': '*' - react-router: ^7.9.6 - typescript: ^5.1.0 - vite: ^5.1.0 || ^6.0.0 || ^7.0.0 - wrangler: ^3.28.2 || ^4.0.0 - peerDependenciesMeta: - '@react-router/serve': - optional: true - '@vitejs/plugin-rsc': - optional: true - typescript: - optional: true - wrangler: - optional: true - '@react-router/express@7.13.2': resolution: {integrity: sha512-OuhenOg3LmCLT23+WA6CU/nIyhGv0/3kmyqpQuXxearj6Gbn1ufI+mkejFWPXsNJf+/y1ttY6P6XL8PzNX5E8w==} engines: {node: '>=20.0.0'} @@ -7891,16 +7852,6 @@ packages: typescript: optional: true - '@react-router/node@7.9.6': - resolution: {integrity: sha512-XzU8gPHwSl2Qh8/bOV30npbpH2fWOO3sFg+SwhX3+IddD1a/0C2KQzRiW/qAngkvZTJVdbca5Qp+FJjCCE7sNw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react-router: 7.9.6 - typescript: ^5.1.0 - peerDependenciesMeta: - typescript: - optional: true - '@react-router/serve@7.13.2': resolution: {integrity: sha512-H/clM2tMw7daRd7bTM0kYYim4ZLrcWd30DY+R/xu8h2t2YvdfLAfHD0GfqGu3Ds8yAOrWFqH5Ly7BM7jk7fvCg==} engines: {node: '>=20.0.0'} @@ -7956,9 +7907,6 @@ packages: '@remix-run/node-fetch-server@0.13.0': resolution: {integrity: sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==} - '@remix-run/node-fetch-server@0.9.0': - resolution: {integrity: sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==} - '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} @@ -9111,12 +9059,8 @@ packages: engines: {node: '>=20.19'} hasBin: true - '@testing-library/dom@10.1.0': - resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} - engines: {node: '>=18'} - - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} '@testing-library/jest-dom@6.6.3': @@ -10319,9 +10263,6 @@ packages: babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} - babel-dead-code-elimination@1.0.9: - resolution: {integrity: sha512-JLIhax/xullfInZjtu13UJjaLHDeTzt3vOeomaSUdO/nAMEL/pWC/laKrSvWylXMnVWyL5bpmG9njqBZlUQOdg==} - babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -11612,8 +11553,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + dompurify@3.4.0: + resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} @@ -12971,8 +12912,8 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.12.12: - resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + hono@4.12.14: + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} engines: {node: '>=16.9.0'} hookable@5.5.3: @@ -12984,10 +12925,6 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - hosted-git-info@6.1.3: - resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -13708,10 +13645,6 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-parse-even-better-errors@3.0.2: - resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - json-pointer@0.6.2: resolution: {integrity: sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==} @@ -14111,10 +14044,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - lucide-react@0.436.0: resolution: {integrity: sha512-N292bIxoqm1aObAg0MzFtvhYwgQE6qnIOWx/GLj5ONgcTPH6N0fD9bVq/GfdeC9ZORBXozt/XeEKDpiB3x3vlQ==} peerDependencies: @@ -15123,10 +15052,6 @@ packages: normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - normalize-package-data@5.0.0: - resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - normalize-path@2.1.1: resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} engines: {node: '>=0.10.0'} @@ -15135,26 +15060,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - npm-install-checks@6.3.0: - resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-normalize-package-bin@3.0.1: - resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - npm-normalize-package-bin@5.0.0: resolution: {integrity: sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==} engines: {node: ^20.17.0 || >=22.9.0} - npm-package-arg@10.1.0: - resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - npm-pick-manifest@8.0.2: - resolution: {integrity: sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - npm-run-all@4.1.5: resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} engines: {node: '>= 4'} @@ -16139,14 +16048,6 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} @@ -16173,8 +16074,8 @@ packages: property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} - protobufjs@7.3.0: - resolution: {integrity: sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==} + protobufjs@7.5.5: + resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -16445,16 +16346,6 @@ packages: peerDependencies: react: '>= 16.3' - react-router@7.12.0: - resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: '>=18' - react-dom: '>=18' - peerDependenciesMeta: - react-dom: - optional: true - react-router@7.13.2: resolution: {integrity: sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==} engines: {node: '>=20.0.0'} @@ -18405,10 +18296,6 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - validate-npm-package-name@5.0.1: - resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - validate.io-array@1.0.6: resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} @@ -18846,11 +18733,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@3.0.1: - resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true - which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} engines: {node: ^16.13.0 || >=18.0.0} @@ -19982,19 +19864,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': - dependencies: - '@babel/core': 7.29.0(supports-color@8.1.1) - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5(supports-color@8.1.1) - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) - '@babel/traverse': 7.29.0(supports-color@8.1.1) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) @@ -20099,7 +19968,7 @@ snapshots: dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) @@ -21384,7 +21253,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.2.3 - protobufjs: 7.3.0 + protobufjs: 7.5.5 yargs: 17.7.2 '@har-sdk/core@1.4.5': @@ -21433,9 +21302,9 @@ snapshots: dependencies: react: 18.3.1 - '@hono/node-server@1.19.13(hono@4.12.12)': + '@hono/node-server@1.19.13(hono@4.12.14)': dependencies: - hono: 4.12.12 + hono: 4.12.14 '@hookform/resolvers@3.3.1(react-hook-form@7.72.1(react@18.3.1))': dependencies: @@ -21926,7 +21795,7 @@ snapshots: '@modelcontextprotocol/sdk@1.27.0(supports-color@8.1.1)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.13(hono@4.12.12) + '@hono/node-server': 1.19.13(hono@4.12.14) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -21936,7 +21805,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1(supports-color@8.1.1) express-rate-limit: 8.3.1(express@5.2.1(supports-color@8.1.1)) - hono: 4.12.12 + hono: 4.12.14 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 @@ -22197,35 +22066,6 @@ snapshots: dependencies: semver: 7.7.4 - '@npmcli/git@4.1.0': - dependencies: - '@npmcli/promise-spawn': 6.0.2 - lru-cache: 7.18.3 - npm-pick-manifest: 8.0.2 - proc-log: 3.0.0 - promise-inflight: 1.0.1 - promise-retry: 2.0.1 - semver: 7.7.4 - which: 3.0.1 - transitivePeerDependencies: - - bluebird - - '@npmcli/package-json@4.0.1': - dependencies: - '@npmcli/git': 4.1.0 - glob: 10.5.0 - hosted-git-info: 6.1.3 - json-parse-even-better-errors: 3.0.2 - normalize-package-data: 5.0.0 - proc-log: 3.0.0 - semver: 7.7.4 - transitivePeerDependencies: - - bluebird - - '@npmcli/promise-spawn@6.0.2': - dependencies: - which: 3.0.1 - '@number-flow/react@0.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: number-flow: 0.3.7 @@ -22990,7 +22830,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.51.1(@opentelemetry/api@1.9.0) - protobufjs: 7.3.0 + protobufjs: 7.5.5 '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.0)': dependencies: @@ -23001,7 +22841,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - protobufjs: 7.3.0 + protobufjs: 7.5.5 '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': dependencies: @@ -23012,7 +22852,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - protobufjs: 7.3.0 + protobufjs: 7.5.5 '@opentelemetry/otlp-transformer@0.51.1(@opentelemetry/api@1.9.0)': dependencies: @@ -25178,7 +25018,7 @@ snapshots: - tsx - yaml - '@react-router/dev@7.9.6(@react-router/serve@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3)': + '@react-router/dev@7.13.2(@react-router/serve@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3)': dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.0 @@ -25187,11 +25027,10 @@ snapshots: '@babel/preset-typescript': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 - '@npmcli/package-json': 4.0.1 - '@react-router/node': 7.9.6(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2) - '@remix-run/node-fetch-server': 0.9.0 + '@react-router/node': 7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2) + '@remix-run/node-fetch-server': 0.13.0 arg: 5.0.2 - babel-dead-code-elimination: 1.0.9(supports-color@8.1.1) + babel-dead-code-elimination: 1.0.12(supports-color@8.1.1) chokidar: 4.0.3 dedent: 1.7.0(babel-plugin-macros@3.1.0) es-module-lexer: 1.7.0 @@ -25202,21 +25041,21 @@ snapshots: p-map: 7.0.4 pathe: 1.1.2 picocolors: 1.1.1 + pkg-types: 2.3.0 prettier: 3.8.1 react-refresh: 0.14.2 - react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - semver: 7.7.3 + react-router: 7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + semver: 7.7.4 tinyglobby: 0.2.15 valibot: 1.2.0(typescript@6.0.2) vite: 8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3) vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3) optionalDependencies: - '@react-router/serve': 7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2) + '@react-router/serve': 7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: - '@types/node' - babel-plugin-macros - - bluebird - jiti - less - lightningcss @@ -25229,15 +25068,6 @@ snapshots: - tsx - yaml - '@react-router/express@7.13.2(express@4.22.1(supports-color@8.1.1))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2)': - dependencies: - '@react-router/node': 7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2) - express: 4.22.1(supports-color@8.1.1) - react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - optionalDependencies: - typescript: 6.0.2 - optional: true - '@react-router/express@7.13.2(express@4.22.1(supports-color@8.1.1))(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2)': dependencies: '@react-router/node': 7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2) @@ -25253,21 +25083,13 @@ snapshots: optionalDependencies: typescript: 6.0.2 - '@react-router/fs-routes@7.4.0(@react-router/dev@7.9.6(@react-router/serve@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3))(typescript@6.0.2)': + '@react-router/fs-routes@7.4.0(@react-router/dev@7.13.2(@react-router/serve@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3))(typescript@6.0.2)': dependencies: - '@react-router/dev': 7.9.6(@react-router/serve@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3) + '@react-router/dev': 7.13.2(@react-router/serve@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@6.0.2)(vite@8.0.8(@types/node@22.13.14)(esbuild@0.25.2)(jiti@2.6.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3) minimatch: 9.0.7 optionalDependencies: typescript: 6.0.2 - '@react-router/node@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2)': - dependencies: - '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - optionalDependencies: - typescript: 6.0.2 - optional: true - '@react-router/node@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 @@ -25275,29 +25097,6 @@ snapshots: optionalDependencies: typescript: 6.0.2 - '@react-router/node@7.9.6(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2)': - dependencies: - '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - optionalDependencies: - typescript: 6.0.2 - - '@react-router/serve@7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2)': - dependencies: - '@mjackson/node-fetch-server': 0.2.0 - '@react-router/express': 7.13.2(express@4.22.1(supports-color@8.1.1))(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2) - '@react-router/node': 7.13.2(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.2) - compression: 1.8.1(supports-color@8.1.1) - express: 4.22.1(supports-color@8.1.1) - get-port: 5.1.1 - morgan: 1.10.1(supports-color@8.1.1) - react-router: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - source-map-support: 0.5.21 - transitivePeerDependencies: - - supports-color - - typescript - optional: true - '@react-router/serve@7.13.2(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(supports-color@8.1.1)(typescript@6.0.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 @@ -25452,8 +25251,6 @@ snapshots: '@remix-run/node-fetch-server@0.13.0': {} - '@remix-run/node-fetch-server@0.9.0': {} - '@repeaterjs/repeater@3.0.6': {} '@resvg/resvg-wasm@2.4.0': {} @@ -26781,26 +26578,15 @@ snapshots: '@tanstack/virtual-file-routes@1.161.7': {} - '@testing-library/dom@10.1.0': + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 '@babel/runtime': 7.26.10 '@types/aria-query': 5.0.2 aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - - '@testing-library/dom@10.4.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.26.10 - '@types/aria-query': 5.0.2 - aria-query: 5.3.0 - chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 + picocolors: 1.1.1 pretty-format: 27.5.1 '@testing-library/jest-dom@6.6.3': @@ -26813,33 +26599,19 @@ snapshots: lodash: 4.18.1 redent: 3.0.0 - '@testing-library/react@16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.0.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.10 - '@testing-library/dom': 10.1.0 + '@testing-library/dom': 10.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@testing-library/react@16.0.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: - '@babel/runtime': 7.26.10 - '@testing-library/dom': 10.4.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - - '@testing-library/user-event@14.6.1(@testing-library/dom@10.1.0)': - dependencies: - '@testing-library/dom': 10.1.0 - - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': - dependencies: - '@testing-library/dom': 10.4.0 + '@testing-library/dom': 10.4.1 '@ts-morph/common@0.23.0': dependencies: @@ -28233,15 +28005,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-dead-code-elimination@1.0.9(supports-color@8.1.1): - dependencies: - '@babel/core': 7.29.0(supports-color@8.1.1) - '@babel/parser': 7.29.0 - '@babel/traverse': 7.29.0(supports-color@8.1.1) - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.26.10 @@ -29660,7 +29423,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.3.3: + dompurify@3.4.0: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -31376,7 +31139,7 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.12.12: {} + hono@4.12.14: {} hookable@5.5.3: {} @@ -31384,10 +31147,6 @@ snapshots: hosted-git-info@2.8.9: {} - hosted-git-info@6.1.3: - dependencies: - lru-cache: 7.18.3 - html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): dependencies: '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) @@ -32061,8 +31820,6 @@ snapshots: json-parse-even-better-errors@2.3.1: {} - json-parse-even-better-errors@3.0.2: {} - json-pointer@0.6.2: dependencies: foreach: 2.0.6 @@ -32457,8 +32214,6 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - lucide-react@0.436.0(react@18.3.1): dependencies: react: 18.3.1 @@ -33003,7 +32758,7 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.13 dayjs: 1.11.18 - dompurify: 3.3.3 + dompurify: 3.4.0 katex: 0.16.22 khroma: 2.1.0 lodash-es: 4.18.1 @@ -33739,7 +33494,7 @@ snapshots: monaco-editor@0.55.1: dependencies: - dompurify: 3.3.3 + dompurify: 3.4.0 marked: 14.0.0 moo@0.5.2: {} @@ -34159,41 +33914,14 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 - normalize-package-data@5.0.0: - dependencies: - hosted-git-info: 6.1.3 - is-core-module: 2.16.1 - semver: 7.7.4 - validate-npm-package-license: 3.0.4 - normalize-path@2.1.1: dependencies: remove-trailing-separator: 1.1.0 normalize-path@3.0.0: {} - npm-install-checks@6.3.0: - dependencies: - semver: 7.7.4 - - npm-normalize-package-bin@3.0.1: {} - npm-normalize-package-bin@5.0.0: {} - npm-package-arg@10.1.0: - dependencies: - hosted-git-info: 6.1.3 - proc-log: 3.0.0 - semver: 7.7.4 - validate-npm-package-name: 5.0.1 - - npm-pick-manifest@8.0.2: - dependencies: - npm-install-checks: 6.3.0 - npm-normalize-package-bin: 3.0.1 - npm-package-arg: 10.1.0 - semver: 7.7.4 - npm-run-all@4.1.5: dependencies: ansi-styles: 3.2.1 @@ -35296,7 +35024,7 @@ snapshots: '@posthog/core': 1.23.1 '@posthog/types': 1.357.0 core-js: 3.44.0 - dompurify: 3.3.3 + dompurify: 3.4.0 fflate: 0.4.8 preact: 10.28.4 query-selector-shadow-dom: 1.0.1 @@ -35354,8 +35082,6 @@ snapshots: progress@2.0.3: {} - promise-inflight@1.0.1: {} - promise-retry@2.0.1: dependencies: err-code: 2.0.3 @@ -35390,7 +35116,7 @@ snapshots: property-information@7.0.0: {} - protobufjs@7.3.0: + protobufjs@7.5.5: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -35687,14 +35413,6 @@ snapshots: transitivePeerDependencies: - react-dom - react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - cookie: 1.0.2 - react: 18.3.1 - set-cookie-parser: 2.7.1 - optionalDependencies: - react-dom: 18.3.1(react@18.3.1) - react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: cookie: 1.0.2 @@ -35880,7 +35598,7 @@ snapshots: classnames: 2.5.1 core-js: 3.44.0 decko: 1.2.0 - dompurify: 3.3.3 + dompurify: 3.4.0 eventemitter3: 5.0.4 json-pointer: 0.6.2 lunr: 2.3.9 @@ -38094,8 +37812,6 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - validate-npm-package-name@5.0.1: {} - validate.io-array@1.0.6: {} validate.io-function@1.0.2: {} @@ -38593,10 +38309,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@3.0.1: - dependencies: - isexe: 2.0.0 - which@4.0.0: dependencies: isexe: 3.1.1 From 8e2aa37c77203bbd2ed122ae31d815bdaf17901e Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Wed, 22 Apr 2026 06:33:43 -0600 Subject: [PATCH 13/63] fix(studio): gate JWT migration behind a flag (#45098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Updated copy on transition from JWT keys to new publishable keys ## Summary by CodeRabbit * **New Features** * JWT settings now toggle between legacy controls and a step-by-step migration guide via a feature flag; legacy rotation/creation modals are hidden when migration mode is enabled. * The “How to change” section is now a collapsible with context-sensitive content and a single link to the full migration guide. * **UX Improvements** * Secrets table action now clearly shows either “Rotate” or “Create standby key” based on key state. * Updated copy and spacing for JWT informational text and external links. * **Chores** * “API Keys” menu now links to the API Keys listing page. --------- Co-authored-by: Joshen Lim Co-authored-by: fadymak --- .../jwt-secret-keys-table/index.tsx | 48 +- .../interfaces/JwtSecrets/jwt-settings.tsx | 416 ++++++++++-------- .../SettingsMenu.utils.tsx | 2 +- .../project/[ref]/settings/jwt/legacy.tsx | 2 +- 4 files changed, 246 insertions(+), 222 deletions(-) diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx index df4281df9c3..ceb2d8c1154 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx @@ -187,7 +187,7 @@ export const JWTSecretKeysTable = () => {
{!canReadAPIKeys ? null : legacyKey ? ( <> - {standbyKey && ( + {standbyKey ? ( { icon={} type="primary" /> - )} - - {!standbyKey && ( + ) : ( {
)} - {/* TODO(hf): For launch
-

Resources

- -
- -
-
- -
-
-

Why Rotate keys?

-

- Create Standby keys ahead of time which can then be promoted to 'In use' at any - time. -

- -
-
-
- - -
-
- -
-
-

Why use a Standby key?

-

- Create Standby keys ahead of time which can then be promoted to 'In use' at any - time. -

- -
-
-
-
-
*/} - diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx index 47eb143e01f..9e9b203e5b0 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-settings.tsx @@ -5,7 +5,7 @@ import { JwtSecretUpdateProgress, JwtSecretUpdateStatus, } from '@supabase/shared-types/out/events' -import { useParams } from 'common' +import { useFlag, useParams } from 'common' import { AlertCircle, ChevronDown, @@ -26,7 +26,9 @@ import { useForm, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' import { Button, - CardContent, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -36,7 +38,6 @@ import { FormControl_Shadcn_, FormField_Shadcn_, FormInputGroupInput, - Input_Shadcn_, InputGroup, InputGroupAddon, InputGroupText, @@ -53,6 +54,7 @@ import { } from './jwt.constants' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FormActions } from '@/components/ui/Forms/FormActions' +import { InlineLink } from '@/components/ui/InlineLink' import Panel from '@/components/ui/Panel' import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query' @@ -88,9 +90,11 @@ const customJwtSecretFormSchema = z.object({ }) const customJwtSecretFormId = 'custom-jwt-secret-form' -const JWTSettings = () => { +export const JWTSettings = () => { const { ref: projectRef } = useParams() + const disableLegacyJwtSecretRotation = useFlag('disableLegacyJwtSecretRotation') + const [customToken, setCustomToken] = useState('') const [isCreatingKey, setIsCreatingKey] = useState(false) const [isRegeneratingKey, setIsGeneratingKey] = useState(false) @@ -114,11 +118,9 @@ const JWTSettings = () => { useJwtSecretUpdateMutation() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: legacyKey } = useLegacyJWTSigningKeyQuery( - { - projectRef, - }, - { enabled: canReadAPIKeys } + const { data: legacyKey, isPending } = useLegacyJWTSigningKeyQuery( + { projectRef }, + { enabled: canReadAPIKeys, retry: false } ) const { data: legacyAPIKeysStatus } = useLegacyAPIKeysStatusQuery( { projectRef }, @@ -238,8 +240,8 @@ const JWTSettings = () => { title="Legacy JWT secret has been migrated to new JWT Signing Keys" >

- Changing the legacy JWT secret can only be done by rotating to a standby key - and then revoking it. It is used to{' '} + Legacy JWT secret can only be changed by rotating to a standby key and then + revoking it. It is used to{' '} {legacyKey.status === 'in_use' ? 'sign and verify' : 'only verify'} {' '} @@ -249,14 +251,15 @@ const JWTSettings = () => { {legacyAPIKeysStatus && legacyAPIKeysStatus.enabled && (

- This includes the anon and service_role JWT - based API keys. + This includes the anon and{' '} + service_role JWT based API + keys. {' '} Consider switching to publishable and secret API keys to disable them.

)} - - + {disableLegacyJwtSecretRotation ? ( +
    +
  1. +

    + Click "Migrate JWT secret" in{' '} + + JWT Signing Keys + + . +

    +

    + This imports your legacy secret into the new system and generates a + standby asymmetric key. +

    +
  2. +
  3. +

    Create and roll out new API keys.

    +

    + In{' '} + + API Keys + + , create a publishable key and secret key, then swap them into your apps + in place of anon and{' '} + service_role{' '} + respectively. Watch the "Last used" indicators to confirm no traffic still + depends on the legacy keys. +

    +
  4. +
  5. +

    + Click "Rotate keys" in{' '} + + JWT Signing Keys + {' '} + to start signing new JWTs with the standby key. +

    +

    + Existing anon,{' '} + service_role, and active user + JWTs stay valid. Before rotating, switch any code that verifies JWTs + directly against the legacy secret (e.g.{' '} + jose,{' '} + jsonwebtoken) to{' '} + supabase.auth.getClaims() or a + JWKS-based verifier, and disable the "Verify JWT" setting on any affected + Edge Functions. +

    +
  6. +
  7. +

    + Optionally, revoke the legacy JWT secret in{' '} + + JWT Signing Keys + {' '} + once you're sure it's no longer in use. +

    +
  8. +
+ ) : ( +
    +
  • Zero-downtime, reversible change.
  • +
  • Users remain signed in and bad actors out.
  • +
  • + Create multiple secret API keys that are immediately revocable and fully + covered by audit logs. +
  • +
  • + Private keys and shared secrets are no longer visible by organization + members, so they can't leak. +
  • +
  • + Maintain tighter alignment with SOC2 and other security compliance + frameworks. +
  • +
  • + Improve app's performance by using public keys to verify JWTs instead of + calling getUser(). +
  • +
+ )} -
- - - - } - loading={isUpdatingJwtSecret} - tooltip={{ - content: { - side: 'bottom', - text: !canGenerateNewJWTSecret - ? 'You need additional permissions to generate a new JWT secret' - : undefined, - }, - }} - > - Change legacy JWT secret - - - - setIsGeneratingKey(true)} - > - -

Generate a random secret

-
- - setIsCreatingKey(true)} - > - -

Create my own secret

-
-
-
-
- +
+ {disableLegacyJwtSecretRotation ? ( + + ) : ( + + + } + loading={isUpdatingJwtSecret} + tooltip={{ + content: { + side: 'bottom', + text: !canGenerateNewJWTSecret + ? 'You need additional permissions to generate a new JWT secret' + : undefined, + }, + }} + > + Change legacy JWT secret + + + + setIsGeneratingKey(true)} + > + +

Generate a random secret

+
+ + setIsCreatingKey(true)} + > + +

Create my own secret

+
+
+
+ )} +
+ + )} @@ -461,7 +531,7 @@ const JWTSettings = () => { { { @@ -623,5 +693,3 @@ const JWTSettings = () => { ) } - -export default JWTSettings diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index 0a15a1364ef..b1733741993 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -83,7 +83,7 @@ export const useGenerateSettingsMenu = () => { { name: 'API Keys', key: 'api-keys', - url: `/project/${ref}/settings/api-keys/new`, + url: `/project/${ref}/settings/api-keys`, items: [], disabled: !isProjectActive, }, diff --git a/apps/studio/pages/project/[ref]/settings/jwt/legacy.tsx b/apps/studio/pages/project/[ref]/settings/jwt/legacy.tsx index d2b341f3d4a..ab8ef11e5ec 100644 --- a/apps/studio/pages/project/[ref]/settings/jwt/legacy.tsx +++ b/apps/studio/pages/project/[ref]/settings/jwt/legacy.tsx @@ -4,7 +4,7 @@ import { useParams } from 'common' import { useEffect, useRef } from 'react' import { toast } from 'sonner' -import JWTSettings from '@/components/interfaces/JwtSecrets/jwt-settings' +import { JWTSettings } from '@/components/interfaces/JwtSecrets/jwt-settings' import { JWT_SECRET_UPDATE_ERROR_MESSAGES } from '@/components/interfaces/JwtSecrets/jwt.constants' import DefaultLayout from '@/components/layouts/DefaultLayout' import JWTKeysLayout from '@/components/layouts/JWTKeys/JWTKeysLayout' From fc0aae8b14f3c40f015eb40f4244e6a5a8d5d4e8 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Wed, 22 Apr 2026 10:12:51 -0230 Subject: [PATCH 14/63] Add new skills page (#45118) Adds a new Skills page CleanShot 2026-04-22 at 09 20 54 ## Summary by CodeRabbit * **New Features** * Home hero now shows two side-by-side CTAs: "Get Started" and "Install Skills" for quick access. * **Documentation** * Added a new "Skills" docs page describing Agent Skills and installation options (CLI and plugin). * Sidebar navigation updated: section renamed to "AI Skills" and the nav item changed to "Skills" (marked new). * **Removed** * Old "Prompts" documentation page removed. --- apps/ui-library/app/(app)/page.tsx | 11 +++-- apps/ui-library/config/docs.ts | 9 ++-- .../content/docs/ai-editors-rules/prompts.mdx | 21 ---------- .../content/docs/ai-editors-rules/skills.mdx | 42 +++++++++++++++++++ 4 files changed, 55 insertions(+), 28 deletions(-) delete mode 100644 apps/ui-library/content/docs/ai-editors-rules/prompts.mdx create mode 100644 apps/ui-library/content/docs/ai-editors-rules/skills.mdx diff --git a/apps/ui-library/app/(app)/page.tsx b/apps/ui-library/app/(app)/page.tsx index e993da7e4cd..567c15e01b5 100644 --- a/apps/ui-library/app/(app)/page.tsx +++ b/apps/ui-library/app/(app)/page.tsx @@ -45,9 +45,14 @@ export default function Home() { A collection of React components and blocks built on the shadcn/ui library that connect your front-end to your Supabase back-end via a single command. - - Get Started - +
+ + Get Started + + + Install Skills + +
diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts index 34c7f4a4893..24a3ac0c1d6 100644 --- a/apps/ui-library/config/docs.ts +++ b/apps/ui-library/config/docs.ts @@ -25,13 +25,14 @@ export const gettingStarted: SidebarNavGroup = { } export const aiEditorsRules: SidebarNavGroup = { - title: 'AI Editors Rules', + title: 'AI Skills', items: [ { - title: 'Prompts', - href: '/docs/ai-editors-rules/prompts', + title: 'Skills', + href: '/docs/ai-editors-rules/skills', items: [], - commandItemLabel: 'AI Editors Rules', + new: true, + commandItemLabel: 'AI Skills', }, ], } diff --git a/apps/ui-library/content/docs/ai-editors-rules/prompts.mdx b/apps/ui-library/content/docs/ai-editors-rules/prompts.mdx deleted file mode 100644 index 3aa4a4457e3..00000000000 --- a/apps/ui-library/content/docs/ai-editors-rules/prompts.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Prompts -description: Rules for AI Code Editors for Supabase ---- - -## Installation - - - -## Folder structure - - - -## Usage - -Running the install command above will add the rule to the `.cursor/rules` directory in your project. If you don't have a `.cursor/rules` directory, it will be created for you. - -Rules are project-specific, so you can have different rules for different projects depending on your needs. Rules are automatically included when matching files are referenced. - -If you're installing the rules in a monorepo, you'll need to move the `.cursor` directory at the -root directory and update all paths in the `.mdc` files. diff --git a/apps/ui-library/content/docs/ai-editors-rules/skills.mdx b/apps/ui-library/content/docs/ai-editors-rules/skills.mdx new file mode 100644 index 00000000000..6c1825c44ac --- /dev/null +++ b/apps/ui-library/content/docs/ai-editors-rules/skills.mdx @@ -0,0 +1,42 @@ +--- +title: Skills +description: Agent Skills for Supabase — procedural knowledge and context that AI agents can load on demand +--- + +Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. Skills give agents access to procedural knowledge and Supabase-specific context they can load on demand, so they can do real work reliably. + +Skills work with 18+ AI agents including Claude Code, GitHub Copilot, Cursor, Cline, and many others. + +## Installation + +Run these commands in the root of your project — skills are installed per-project, similar to how you'd add a config file or dev dependency. Once installed, your AI agent will automatically pick them up the next time it runs. + +Install all Supabase skills using the skills CLI: + +```bash +npx skills add supabase/agent-skills +``` + +This installs the full set of Supabase skills — the right choice for most projects. If you're doing a lot of database work, also add the Postgres-specific skill: + +```bash +npx skills add supabase/agent-skills --skill supabase-postgres-best-practices +``` + +The `supabase-postgres-best-practices` skill covers Postgres performance optimization, query design, and schema best practices. It's used automatically when writing, reviewing, or optimizing queries and database configurations. + +### Claude Code + +You can also install the skills as Claude Code plugins: + +```bash +/plugin marketplace add supabase/agent-skills +/plugin install supabase@supabase-agent-skills +``` + +## Learn more + +For the full list of available skills, installation options, and usage guidance, see the [Agent Skills documentation](https://supabase.com/docs/guides/getting-started/ai-skills). + +- [Agent Skills Repository](https://github.com/supabase/agent-skills) +- [Agent Skills Documentation](https://agentskills.io/home) From 3838e32d33ccf7e40c924d586151fb7f39b09bd4 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Wed, 22 Apr 2026 06:53:13 -0600 Subject: [PATCH 15/63] chore(studio): migrate useKeyboardShortcuts to useShortcut (#45100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Follow-up to #45099 — migrates the final legacy hotkey hook (`useKeyboardShortcuts` in `apps/studio/components/grid/components/common/Hooks.tsx`) to `useShortcut`, backed by `SHORTCUT_DEFINITIONS`. - Adds 4 registry entries (`TABLE_EDITOR_JUMP_FIRST_ROW` / `_LAST_ROW` / `_FIRST_COL` / `_LAST_COL`), all `showInSettings: false` so they stay non-configurable (same as today). - Adds `ignoreInputs` to `ShortcutOptions` and threads it through `useShortcut` → `useHotkeySequence`. Replaces the legacy `whitelistNodes: ['INPUT', 'TEXTAREA', 'SELECT']` with the built-in TanStack option, which also covers `contenteditable`. - Drops the `navigator.appVersion` macOS/Windows detection in `Shortcuts.tsx` (`Mod` resolves this automatically) and the manual `event.stopPropagation()` calls (TanStack's default is `stopPropagation: true`). - Deletes `Hooks.tsx` — no remaining consumers. Closes FE-3049. ## Why `ignoreInputs: true` on these entries `Mod+Arrow*` is a Ctrl/Meta combo. TanStack's default for Ctrl/Meta combos is `ignoreInputs: false`, which would fire the shortcut even when focus is in a text input — not what we want. Setting `ignoreInputs: true` in the registry preserves the legacy whitelist behavior. ## Test plan All shortcuts should still fire with **Cmd** (macOS) / **Ctrl** (Win/Linux). Test surface: **Table Editor** → open any table with rows and multiple columns. **Grid navigation (with a cell selected — click a cell first)** - [x] `Cmd+ArrowUp` jumps selection to row 0 in the same column - [x] `Cmd+ArrowDown` jumps selection to the last row in the same column - [x] `Cmd+ArrowLeft` jumps selection to the first non-frozen column in the same row - [x] `Cmd+ArrowRight` jumps selection to the last column in the same row (not the trailing "add column" cell) **Whitelist regression (most important — the `ignoreInputs` check)** - [x] Open the **Filter** popover on a table → put focus in the filter value `` → type with arrow keys / use `Cmd+ArrowLeft/Right` for word-jump → cursor moves in the input, grid selection does **not** jump - [x] Same for a `