From 97d234a56653a8d7b1f1d48eae39f1e044d7feb0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 9 Apr 2026 09:38:18 +0000 Subject: [PATCH 01/18] :sparkles: Add 2h min-age threshold to storage/gc_touched task Skip storage objects touched less than 2 hours ago, matching the pattern used by upload-session-gc. Update all affected tests to advance the clock past the threshold using ct/*clock* bindings. --- backend/src/app/storage/gc_touched.clj | 13 ++-- backend/test/backend_tests/rpc_file_test.clj | 24 ++++--- .../rpc_file_thumbnails_test.clj | 7 +- backend/test/backend_tests/rpc_font_test.clj | 71 ++++++++----------- backend/test/backend_tests/storage_test.clj | 17 +++-- 5 files changed, 71 insertions(+), 61 deletions(-) diff --git a/backend/src/app/storage/gc_touched.clj b/backend/src/app/storage/gc_touched.clj index a3ea21e44d..f00140d04e 100644 --- a/backend/src/app/storage/gc_touched.clj +++ b/backend/src/app/storage/gc_touched.clj @@ -213,8 +213,13 @@ [_ params] (assert (db/pool? (::db/pool params)) "expect valid storage")) -(defmethod ig/init-key ::handler - [_ cfg] - (fn [_] - (process-touched! (assoc cfg ::timestamp (ct/now))))) +(defmethod ig/expand-key ::handler + [k v] + {k (merge {::min-age (ct/duration {:hours 2})} v)}) + +(defmethod ig/init-key ::handler + [_ {:keys [::min-age] :as cfg}] + (fn [_] + (let [threshold (ct/minus (ct/now) min-age)] + (process-touched! (assoc cfg ::timestamp threshold))))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 921477d1b3..281c834256 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -312,7 +312,8 @@ ;; freeze because of the deduplication (we have uploaded 2 times ;; the same files). - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -386,7 +387,8 @@ ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of ;; them are marked to be deleted - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) @@ -571,7 +573,8 @@ ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of ;; them are marked to be deleted. - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) @@ -664,7 +667,8 @@ ;; because of the deduplication (we have uploaded 2 times the ;; same files). - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -714,7 +718,8 @@ ;; Now that objects-gc have deleted the object thumbnail lets ;; execute the touched-gc task - (let [res (th/run-task! "storage-gc-touched" {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! "storage-gc-touched" {}))] (t/is (= 1 (:freeze res)))) ;; check file media objects @@ -749,7 +754,8 @@ ;; Now that file-gc have deleted the object thumbnail lets ;; execute the touched-gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:delete res)))) ;; check file media objects @@ -1319,7 +1325,8 @@ ;; The FileGC task will schedule an inner taskq (th/run-pending-tasks!) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -1413,7 +1420,8 @@ ;; we ensure that once object-gc is passed and marked two storage ;; objects to delete - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index 9a856f3210..28134da5ff 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -85,7 +85,7 @@ (t/is (map? (:result out)))) ;; run the task again - (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] (th/run-task! "storage-gc-touched" {}))] (t/is (= 2 (:freeze res)))) @@ -136,7 +136,7 @@ (t/is (some? (sto/get-object storage (:media-id row2)))) ;; run the task again - (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:delete res))) (t/is (= 0 (:freeze res)))) @@ -235,7 +235,8 @@ (t/is (= (:object-id data1) (:object-id row))) (t/is (uuid? (:media-id row1)))) - (let [result (th/run-task! :storage-gc-touched {})] + (let [result (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 1 (:delete result)))) ;; Check if storage objects still exists after file-gc diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index 1316b237c9..d68f657c59 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -130,7 +130,8 @@ ;; (th/print-result! out) (t/is (nil? (:error out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 6 (:freeze res)))) (let [params {::th/type :delete-font @@ -142,14 +143,16 @@ (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (let [res (th/run-task! :objects-gc {})] - (t/is (= 2 (:processed res)))) + (t/is (= 2 (:processed res))))) + (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8 :hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 6 (:delete res))))))) @@ -191,7 +194,8 @@ ;; (th/print-result! out) (t/is (nil? (:error out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 6 (:freeze res)))) (let [params {::th/type :delete-font @@ -203,14 +207,16 @@ (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (let [res (th/run-task! :objects-gc {})] - (t/is (= 1 (:processed res)))) + (t/is (= 1 (:processed res))))) + (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8 :hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 3 (:delete res))))))) @@ -220,57 +226,42 @@ team-id (:default-team-id prof) proj-id (:default-project-id prof) font-id (uuid/custom 10 1) - - data1 (-> (io/resource "backend_tests/test_files/font-1.woff") - (io/read*)) - - data2 (-> (io/resource "backend_tests/test_files/font-2.woff") - (io/read*)) - params1 {::th/type :create-font-variant - ::rpc/profile-id (:id prof) - :team-id team-id - :font-id font-id - :font-family "somefont" - :font-weight 400 - :font-style "normal" - :data {"font/woff" data1}} - - params2 {::th/type :create-font-variant - ::rpc/profile-id (:id prof) - :team-id team-id - :font-id font-id - :font-family "somefont" - :font-weight 500 - :font-style "normal" - :data {"font/woff" data2}} - + data1 (-> (io/resource "backend_tests/test_files/font-1.woff") (io/read*)) + data2 (-> (io/resource "backend_tests/test_files/font-2.woff") (io/read*)) + params1 {::th/type :create-font-variant ::rpc/profile-id (:id prof) + :team-id team-id :font-id font-id :font-family "somefont" + :font-weight 400 :font-style "normal" :data {"font/woff" data1}} + params2 {::th/type :create-font-variant ::rpc/profile-id (:id prof) + :team-id team-id :font-id font-id :font-family "somefont" + :font-weight 500 :font-style "normal" :data {"font/woff" data2}} out1 (th/command! params1) out2 (th/command! params2)] - - ;; (th/print-result! out1) (t/is (nil? (:error out1))) (t/is (nil? (:error out2))) - (let [res (th/run-task! :storage-gc-touched {})] + ;; freeze with hours 3 clock + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 6 (:freeze res)))) - (let [params {::th/type :delete-font-variant - ::rpc/profile-id (:id prof) - :team-id team-id - :id (-> out1 :result :id)} + (let [params {::th/type :delete-font-variant ::rpc/profile-id (:id prof) + :team-id team-id :id (-> out1 :result :id)} out (th/command! params)] - ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (nil? (:result out)))) - (let [res (th/run-task! :storage-gc-touched {})] + ;; no-op with hours 3 clock (nothing touched yet) + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) + ;; objects-gc at days 8, then storage-gc-touched at days 8 + 3h (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (let [res (th/run-task! :objects-gc {})] - (t/is (= 1 (:processed res)))) + (t/is (= 1 (:processed res))))) + (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8 :hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 3 (:delete res))))))) diff --git a/backend/test/backend_tests/storage_test.clj b/backend/test/backend_tests/storage_test.clj index cd058af250..027d54ce70 100644 --- a/backend/test/backend_tests/storage_test.clj +++ b/backend/test/backend_tests/storage_test.clj @@ -169,7 +169,8 @@ (t/is (= 2 (:count res)))) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -229,7 +230,8 @@ (t/is (nil? (:error out2))) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 5 (:freeze res))) (t/is (= 0 (:delete res))) @@ -249,7 +251,8 @@ (th/db-exec-one! ["update storage_object set touched_at=?" (ct/now)]) ;; Run the task again - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 3 (:delete res)))) @@ -295,7 +298,8 @@ (th/db-exec! ["update storage_object set touched_at=?" (ct/now)]) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 2 (:freeze res))) (t/is (= 0 (:delete res)))) @@ -310,7 +314,8 @@ (t/is (= 2 (:processed res)))) ;; run the touched gc task - (let [res (th/run-task! :storage-gc-touched {})] + (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] + (th/run-task! :storage-gc-touched {}))] (t/is (= 0 (:freeze res))) (t/is (= 2 (:delete res)))) @@ -336,7 +341,7 @@ (t/is (= 0 (:delete res))))) - (binding [ct/*clock* (ct/fixed-clock (ct/plus now {:minutes 1}))] + (binding [ct/*clock* (ct/fixed-clock (ct/plus now {:hours 3}))] (let [res (th/run-task! :storage-gc-touched {})] (t/is (= 0 (:freeze res))) (t/is (= 1 (:delete res))))) From 6ea7a64e011bdc2005dfe767c0e2c8dd7712177d Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Wed, 22 Apr 2026 09:33:58 +0200 Subject: [PATCH 02/18] :sparkles: Add nginx configuration for mcp server --- docker/images/files/nginx-entrypoint.sh | 3 ++- docker/images/files/nginx.conf.template | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 4512d06495..772879099f 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -30,8 +30,9 @@ update_flags /var/www/app/js/config.js export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060} export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061} export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000} +export PENPOT_MCP_URI=${PENPOT_MCP_URI:-http://penpot-mcp} export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB -envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \ +envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_MCP_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \ < /tmp/nginx.conf.template > /etc/nginx/nginx.conf PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)" diff --git a/docker/images/files/nginx.conf.template b/docker/images/files/nginx.conf.template index d0b7bc3b1f..beec6ea913 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -135,6 +135,23 @@ http { proxy_http_version 1.1; } + location /mcp/ws { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://$PENPOT_MCP_URI:4402; + proxy_http_version 1.1; + } + + location /mcp/stream { + proxy_pass http://$PENPOT_MCP_URI:4401/mcp; + proxy_http_version 1.1; + } + + location /mcp/sse { + proxy_pass http://$PENPOT_MCP_URI:4401/sse; + proxy_http_version 1.1; + } + location /readyz { access_log off; proxy_pass $PENPOT_BACKEND_URI$request_uri; From 98e816087503d6df9f33cb48243dceea8d8dfeb2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 31 Mar 2026 21:28:09 +0000 Subject: [PATCH 03/18] :recycle: Remove worker URI from global templates and compute from public URI - Remove penpotWorkerURI from index.mustache and rasterizer.mustache templates - Remove worker_main entry from the build manifest - Construct worker URI in config.cljs by joining public-uri with worker path - Fix global variable casing for plugins-list-uri and templates-uri - Fix alignment in worker.cljs let bindings --- frontend/resources/templates/index.mustache | 1 - frontend/resources/templates/rasterizer.mustache | 1 - frontend/scripts/_helpers.js | 1 - frontend/src/app/config.cljs | 9 ++++++--- frontend/src/app/util/worker.cljs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index f80b7e7759..60c6119fd0 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -31,7 +31,6 @@ globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersionTag = "{{& version_tag}}"; globalThis.penpotBuildDate = "{{& build_date}}"; - globalThis.penpotWorkerURI = "{{& manifest.worker_main}}"; {{# manifest}} diff --git a/frontend/resources/templates/rasterizer.mustache b/frontend/resources/templates/rasterizer.mustache index 90a7f1dfdc..6a3d815e29 100644 --- a/frontend/resources/templates/rasterizer.mustache +++ b/frontend/resources/templates/rasterizer.mustache @@ -9,7 +9,6 @@ globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersionTag = "{{& version_tag}}"; globalThis.penpotBuildDate = "{{& build_date}}"; - globalThis.penpotWorkerURI = "{{& manifest.worker_main}}"; {{# manifest}} diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index dd2e23c348..c3c9a9d575 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -209,7 +209,6 @@ async function generateManifest() { config: "./js/config.js?version=" + VERSION_TAG, polyfills: "./js/polyfills.js?version=" + VERSION_TAG, libs: "./js/libs.js?version=" + VERSION_TAG, - worker_main: "./js/worker/main.js?version=" + VERSION_TAG, default_translations: "./js/translation.en.js?version=" + VERSION_TAG, importmap: JSON.stringify({ diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 75f5010280..4be493ef52 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -153,9 +153,9 @@ (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) -(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot.app/penpothub/plugins")) +(def plugins-list-uri (obj/get global "penpotPluginsListURI" "https://penpot.app/penpothub/plugins")) (def plugins-whitelist (into #{} (obj/get global "penpotPluginsWhitelist" []))) -(def templates-uri (obj/get global "penpotTemplatesUri" "https://penpot.github.io/penpot-files/")) +(def templates-uri (obj/get global "penpotTemplatesURI" "https://penpot.github.io/penpot-files/")) ;; We set the current parsed flags under common for make ;; it available for common code without the need to pass @@ -177,7 +177,10 @@ public-uri)) (def worker-uri - (obj/get global "penpotWorkerURI" "/js/worker/main.js")) + (-> public-uri + (u/join "js/worker/main.js") + (get :path) + (str "?version=" version-tag))) (defn external-feature-flag [flag value] diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index b23bbbee92..8d87a76795 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -90,8 +90,8 @@ "Return a initialized webworker instance." [path on-error] (let [instance (js/Worker. path) - bus (rx/subject) - worker (Worker. instance (rx/to-observable bus)) + bus (rx/subject) + worker (Worker. instance (rx/to-observable bus)) handle-message (fn [event] From 47b366724858616774ec0ca9b28071bbab8a855e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 31 Mar 2026 21:41:46 +0000 Subject: [PATCH 04/18] :bug: Fix exporter renderer URI path construction Apply consistent path construction across bitmap, PDF, and SVG renderers in the exporter. Use path join utilities instead of hardcoding the render.html path, ensuring the path is properly appended to the public URI base path. - bitmap.cljs: Use u/ensure-path-slash and u/join for path - pdf.cljs: Use u/join and ensure-path-slash on base-uri - svg.cljs: Use u/ensure-path-slash and u/join for path --- exporter/src/app/renderer/bitmap.cljs | 3 ++- exporter/src/app/renderer/pdf.cljs | 5 +++-- exporter/src/app/renderer/svg.cljs | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index b1df4a7447..5d81eebb9f 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -60,6 +60,7 @@ :route "objects" :skip-children skip-children} uri (-> (cf/get :public-uri) - (assoc :path "/render.html") + (u/ensure-path-slash) + (u/join "render.html") (assoc :query (u/map->query-string params)))] (bw/exec! (prepare-options uri) (partial render uri))))) diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index 25bcfc036b..edfdcda1b1 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -35,7 +35,7 @@ :object-id object-id :route "objects"}] (-> base-uri - (assoc :path "/render.html") + (u/join "render.html") (assoc :query (u/map->query-string params))))) (sync-page-size! [dom] @@ -76,6 +76,7 @@ (on-object (assoc object :path path)) (p/recur (rest objects))))))] - (let [base-uri (cf/get :public-uri)] + (let [base-uri (-> (cf/get :public-uri) + (u/ensure-path-slash))] (bw/exec! (prepare-options base-uri) (partial render base-uri))))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 73558dbe5f..71da424fb3 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -349,7 +349,8 @@ :object-id (mapv :id objects) :route "objects"} uri (-> (cf/get :public-uri) - (assoc :path "/render.html") + (u/ensure-path-slash) + (u/join "render.html") (assoc :query (u/map->query-string params)))] (bw/exec! (prepare-options uri) (partial render uri))))) From 6de5370a0bdd4f22f3c62bd2442dff9ec1c0d41a Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Wed, 22 Apr 2026 10:29:52 +0200 Subject: [PATCH 05/18] :bug: Fix nginx configuration for mcp --- docker/images/files/nginx.conf.template | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/images/files/nginx.conf.template b/docker/images/files/nginx.conf.template index beec6ea913..0daab4b9d4 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -138,17 +138,17 @@ http { location /mcp/ws { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; - proxy_pass http://$PENPOT_MCP_URI:4402; + proxy_pass $PENPOT_MCP_URI:4402; proxy_http_version 1.1; } location /mcp/stream { - proxy_pass http://$PENPOT_MCP_URI:4401/mcp; + proxy_pass $PENPOT_MCP_URI:4401/mcp; proxy_http_version 1.1; } location /mcp/sse { - proxy_pass http://$PENPOT_MCP_URI:4401/sse; + proxy_pass $PENPOT_MCP_URI:4401/sse; proxy_http_version 1.1; } From 3225319e0c45bf94e115e89fd65137b097e649a9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:54:07 +0200 Subject: [PATCH 06/18] :bug: Fix frontend tests --- .../logic/components_and_tokens.cljs | 8 ++++---- .../tokens/logic/token_actions_test.cljs | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 689ac76b63..9e830c9a76 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -141,7 +141,7 @@ events [(dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-2") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -249,11 +249,11 @@ events [(dwta/apply-token {:shape-ids [(cthi/id :c-frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-2") - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -293,7 +293,7 @@ (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index cb6f2e39d8..e468e420bb 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -64,7 +64,7 @@ events [(dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.md") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -89,11 +89,11 @@ events [(dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) (dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.md") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -117,14 +117,14 @@ (dwta/apply-token {:attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") :shape-ids [(:id rect-1)] - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) ;; Apply single `:r1` attribute to same shape ;; while removing other attributes from the border-radius set ;; but keep `:r4` for testing purposes (dwta/apply-token {:attributes #{:r1 :r2 :r3} :token (toht/get-token file "borderRadius.md") :shape-ids [(:id rect-1)] - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -153,7 +153,7 @@ (dwta/apply-token {:shape-ids [(:id rect-2)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -762,7 +762,7 @@ rect-2 (cths/get-shape file :rect-2) events [(dwta/toggle-token {:shape-ids [(:id rect-1) (:id rect-2)] :token-type-props {:attributes #{:r1 :r2 :r3 :r4} - :on-update-shape dwta/update-shape-radius-all} + :on-update-shape dwta/update-shape-radius} :token (toht/get-token file "borderRadius.md")})]] (tohs/run-store-async store done events From 09637f9794d671bc08b95669a9e3c97c4b9d2955 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 10:01:38 +0200 Subject: [PATCH 07/18] :sparkles: Allow render entrypoint load alternative config The render entrypoint is used by exporter --- frontend/resources/templates/render.mustache | 2 +- frontend/scripts/_helpers.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache index 4de213f9ad..67629b075e 100644 --- a/frontend/resources/templates/render.mustache +++ b/frontend/resources/templates/render.mustache @@ -14,7 +14,7 @@ {{# manifest}} - + {{/manifest}} diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index c3c9a9d575..240c6b0e45 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -207,6 +207,7 @@ async function generateManifest() { rasterizer_main: "./js/rasterizer.js", config: "./js/config.js?version=" + VERSION_TAG, + config_render: "./js/config-render.js?version=" + VERSION_TAG, polyfills: "./js/polyfills.js?version=" + VERSION_TAG, libs: "./js/libs.js?version=" + VERSION_TAG, default_translations: "./js/translation.en.js?version=" + VERSION_TAG, From 75d99a07256e62432d39bab2a51dea2d973fd2cc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:32:47 +0200 Subject: [PATCH 08/18] :wrench: Add missing public uri handling on nginx entrypoint --- docker/images/files/nginx-entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 772879099f..9ce2b9261d 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -19,6 +19,10 @@ update_flags() { -e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \ "$1")" > "$1" fi + + if [ -n "$PENPOT_PUBLIC_URI" ]; then + echo "var penpotPublicURI = \"$PENPOT_PUBLIC_URI\";" >> "$1"; + fi } update_flags /var/www/app/js/config.js From 88008ce16c9ec7fdd9c46b64a8709c6009d1fd15 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 12:34:32 +0200 Subject: [PATCH 09/18] :paperclip: Update mcp types yaml file --- mcp/packages/server/data/api_types.yml | 49 +++++++++++++++++++------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index 901037c249..54b4100e4a 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -26,6 +26,7 @@ Penpot: props?: { [key: string]: unknown }, ): symbol; off(listenerId: symbol): void; + version: string; root: Shape | null; currentFile: File | null; currentPage: Page | null; @@ -72,7 +73,7 @@ Penpot: generateFontFaces(shapes: Shape[]): Promise; openViewer(): void; createPage(): Page; - openPage(page: Page, newWindow?: boolean): void; + openPage(page: string | Page, newWindow?: boolean): void; alignHorizontal( shapes: Shape[], direction: "center" | "left" | "right", @@ -162,6 +163,12 @@ Penpot: ``` penpot.closePlugin(); ``` + version: |- + ``` + readonly version: string + ``` + + Returns the current penpot version. root: |- ``` readonly root: Shape | null @@ -725,19 +732,19 @@ Penpot: Returns Page openPage: |- ``` - openPage(page: Page, newWindow?: boolean): void + openPage(page: string | Page, newWindow?: boolean): void ``` Changes the current open page to given page. Requires `content:read` permission. Parameters - * page: Page + * page: string | Page - the page to open + the page to open (a Page object or a page UUID string) * newWindow: boolean - if true opens the page in a new window + if true opens the page in a new window, defaults to false Returns void @@ -4785,6 +4792,7 @@ Context: ``` interface Context { + version: string; root: Shape | null; currentFile: File | null; currentPage: Page | null; @@ -4837,7 +4845,7 @@ Context: removeListener(listenerId: symbol): void; openViewer(): void; createPage(): Page; - openPage(page: Page, newWindow?: boolean): void; + openPage(page: string | Page, newWindow?: boolean): void; alignHorizontal( shapes: Shape[], direction: "center" | "left" | "right", @@ -4854,6 +4862,12 @@ Context: ``` members: Properties: + version: |- + ``` + readonly version: string + ``` + + Returns the current penpot version. root: |- ``` readonly root: Shape | null @@ -5392,19 +5406,19 @@ Context: Returns Page openPage: |- ``` - openPage(page: Page, newWindow?: boolean): void + openPage(page: string | Page, newWindow?: boolean): void ``` Changes the current open page to given page. Requires `content:read` permission. Parameters - * page: Page + * page: string | Page - the page to open + the page to open (a Page object or a page UUID string) * newWindow: boolean - if true opens the page in a new window + if true opens the page in a new window, defaults to false Returns void @@ -6845,7 +6859,7 @@ Export: ``` interface Export { - type: "svg" | "png" | "jpeg" | "pdf"; + type: "svg" | "png" | "jpeg" | "webp" | "pdf"; scale?: number; suffix?: string; skipChildren?: boolean; @@ -6857,10 +6871,10 @@ Export: Properties: type: |- ``` - type: "svg" | "png" | "jpeg" | "pdf" + type: "svg" | "png" | "jpeg" | "webp" | "pdf" ``` - Type of the file to export. Can be one of the following values: png, jpeg, svg, pdf + Type of the file to export. Can be one of the following values: png, jpeg, webp, svg, pdf scale: |- ``` scale?: number @@ -7249,6 +7263,7 @@ Flags: ``` interface Flags { naturalChildOrdering: boolean; + throwValidationErrors: boolean; } ``` @@ -7264,6 +7279,14 @@ Flags: Also, appendChild method will be append the children in the top-most position. The insertchild method is changed acordingly to respect this ordering. Defaults to false + throwValidationErrors: |- + ``` + throwValidationErrors: boolean + ``` + + If `true` the validation errors will throw an exception instead of displaying an + error in the debugger console. + Defaults to false FlexLayout: overview: |- Interface FlexLayout From b0b2c0d26480eab35bf279e02caddc293b43f53c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 13:18:24 +0200 Subject: [PATCH 10/18] :paperclip: Update version on mcp/ module --- mcp/package.json | 2 +- mcp/scripts/set-version | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 mcp/scripts/set-version diff --git a/mcp/package.json b/mcp/package.json index 6324fe3898..c407617a50 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@penpot/mcp", - "version": "2.15.0-rc.1.153", + "version": "2.15.0-rc.1", "description": "MCP server for Penpot integration", "bin": { "penpot-mcp": "./bin/mcp-local.js" diff --git a/mcp/scripts/set-version b/mcp/scripts/set-version old mode 100644 new mode 100755 From 3c542a1abc1c4409c0a46b985474692606801e0b Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Wed, 22 Apr 2026 15:59:28 +0200 Subject: [PATCH 11/18] :bug: Fix email validation (#9037) --- CHANGES.md | 6 ++ common/src/app/common/spec.cljc | 20 ++++-- common/test/common_tests/runner.cljc | 2 + common/test/common_tests/spec_test.cljc | 89 +++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 common/test/common_tests/spec_test.cljc diff --git a/CHANGES.md b/CHANGES.md index 0d431c0d2b..774d17f3de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # CHANGELOG +## 2.14.4 (Unreleased) + +### :bug: Bugs fixed + +- Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006) + ## 2.14.3 ### :sparkles: New features & Enhancements diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index 38af563499..d6f0d6cacc 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -113,12 +113,19 @@ (tgen/fmap keyword))))) ;; --- SPEC: email +;; +;; Regex rules enforced: +;; local part - valid RFC chars, no leading/trailing dot, no consecutive dots +;; domain - labels can't start/end with hyphen, no empty labels +;; TLD - at least 2 alphabetic chars -(def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") +(def email-re + #"^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,63}$") (defn parse-email [s] - (some->> s (re-seq email-re) first)) + (when (and (string? s) (re-matches email-re s)) + s)) (letfn [(conformer [v] (or (parse-email v) ::s/invalid)) @@ -126,11 +133,10 @@ (dm/str v))] (s/def ::email (s/with-gen (s/conformer conformer unformer) - #(as-> (tgen/let [p1 (s/gen ::not-empty-string) - p2 (s/gen ::not-empty-string) - p3 (tgen/elements ["com" "net"])] - (str p1 "@" p2 "." p3)) $ - (tgen/such-that (partial re-matches email-re) $ 50))))) + #(tgen/let [local (tgen/string-alphanumeric 1 20) + label (tgen/string-alphanumeric 2 10) + tld (tgen/elements ["com" "net" "org" "io" "co" "dev"])] + (str local "@" label "." tld))))) ;; -- SPEC: uri diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 2d9a216cbc..b3c2ad4f0e 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -49,6 +49,7 @@ [common-tests.path-names-test] [common-tests.record-test] [common-tests.schema-test] + [common-tests.spec-test] [common-tests.svg-path-test] [common-tests.svg-test] [common-tests.text-test] @@ -122,6 +123,7 @@ 'common-tests.path-names-test 'common-tests.record-test 'common-tests.schema-test + 'common-tests.spec-test 'common-tests.svg-path-test 'common-tests.svg-test 'common-tests.text-test diff --git a/common/test/common_tests/spec_test.cljc b/common/test/common_tests/spec_test.cljc new file mode 100644 index 0000000000..425f7f8066 --- /dev/null +++ b/common/test/common_tests/spec_test.cljc @@ -0,0 +1,89 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.spec-test + (:require + [app.common.spec :as spec] + [clojure.test :as t])) + +(t/deftest valid-emails + (t/testing "accepts well-formed email addresses" + (doseq [email ["user@domain.com" + "user.name@domain.com" + "user+tag@domain.com" + "user-name@domain.com" + "user_name@domain.com" + "user123@domain.com" + "USER@DOMAIN.COM" + "u@domain.io" + "user@sub.domain.com" + "user@domain.co.uk" + "user@domain.dev" + "a@bc.co"]] + (t/is (some? (spec/parse-email email)) (str "should accept: " email))))) + +(t/deftest rejects-invalid-local-part + (t/testing "rejects local part starting with a dot" + (t/is (nil? (spec/parse-email ".user@domain.com")))) + + (t/testing "rejects local part with consecutive dots" + (t/is (nil? (spec/parse-email "user..name@domain.com")))) + + (t/testing "rejects local part with spaces" + (t/is (nil? (spec/parse-email "us er@domain.com")))) + + (t/testing "rejects local part with comma" + (t/is (nil? (spec/parse-email "user,name@domain.com"))) + (t/is (nil? (spec/parse-email ",user@domain.com")))) + + (t/testing "rejects empty local part" + (t/is (nil? (spec/parse-email "@domain.com"))))) + +(t/deftest rejects-invalid-domain + (t/testing "rejects domain starting with a dot" + (t/is (nil? (spec/parse-email "user@.domain.com")))) + + (t/testing "rejects domain part with comma" + (t/is (nil? (spec/parse-email "user@domain,com"))) + (t/is (nil? (spec/parse-email "user@,domain.com")))) + + (t/testing "rejects domain with consecutive dots" + (t/is (nil? (spec/parse-email "user@sub..domain.com")))) + + (t/testing "rejects label starting with hyphen" + (t/is (nil? (spec/parse-email "user@-domain.com")))) + + (t/testing "rejects label ending with hyphen" + (t/is (nil? (spec/parse-email "user@domain-.com")))) + + (t/testing "rejects TLD shorter than 2 chars" + (t/is (nil? (spec/parse-email "user@domain.c")))) + + (t/testing "rejects domain without a dot" + (t/is (nil? (spec/parse-email "user@domain")))) + + (t/testing "rejects domain with spaces" + (t/is (nil? (spec/parse-email "user@do main.com")))) + + (t/testing "rejects domain ending with a dot" + (t/is (nil? (spec/parse-email "user@domain."))))) + +(t/deftest rejects-invalid-structure + (t/testing "rejects nil" + (t/is (nil? (spec/parse-email nil)))) + + (t/testing "rejects empty string" + (t/is (nil? (spec/parse-email "")))) + + (t/testing "rejects string without @" + (t/is (nil? (spec/parse-email "userdomain.com")))) + + (t/testing "rejects string with multiple @" + (t/is (nil? (spec/parse-email "user@@domain.com"))) + (t/is (nil? (spec/parse-email "us@er@domain.com")))) + + (t/testing "rejects empty domain" + (t/is (nil? (spec/parse-email "user@"))))) From b60695f54a9c3a6a19e94feccb6dd59f4b762614 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Wed, 22 Apr 2026 15:17:46 +0200 Subject: [PATCH 12/18] :bug: Fix indicate that the mcp is disabled if the mcp key has expired If the mcp key has expired, the switch that indicates the status in the dashboard will appear as disabled, and will show a modal for regenerate the key. It will also appear as disabled in the workspace, not allowing the plugin to connect --- frontend/src/app/main/refs.cljs | 6 +++++ .../app/main/ui/settings/integrations.cljs | 26 +++++++++---------- .../src/app/main/ui/workspace/main_menu.cljs | 26 +++++++++++-------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 30c431ac86..9e1c13cd58 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -648,3 +648,9 @@ (def progress (l/derived :progress st/state)) + +(def access-tokens + (l/derived :access-tokens st/state)) + +(def access-token-created + (l/derived :access-token-created st/state)) diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index 780f9918e3..ff4b39049e 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -34,15 +34,8 @@ [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :as i18n :refer [tr]] - [okulary.core :as l] [rumext.v2 :as mf])) -(def tokens-ref - (l/derived :access-tokens st/state)) - -(def token-created-ref - (l/derived :access-token-created st/state)) - (def notification-timeout 7000) (def ^:private schema:form-access-token @@ -78,7 +71,7 @@ (mf/defc token-created* {::mf/private true} [{:keys [title mcp-key?]}] - (let [token-created (mf/deref token-created-ref) + (let [token-created (mf/deref refs/access-token-created) on-copy-to-clipboard (mf/use-fn @@ -310,7 +303,7 @@ [] (let [created? (mf/use-state false) - tokens (mf/deref tokens-ref) + tokens (mf/deref refs/access-tokens) mcp-key (some #(when (= (:type %) "mcp") %) tokens) mcp-key-id (:id mcp-key) @@ -413,7 +406,7 @@ (mf/defc mcp-server-section* {::mf/private true} [] - (let [tokens (mf/deref tokens-ref) + (let [tokens (mf/deref refs/access-tokens) profile (mf/deref refs/profile) mcp-key (some #(when (= (:type %) "mcp") %) tokens) @@ -422,6 +415,8 @@ expires-at (:expires-at mcp-key) expired? (and (some? expires-at) (> (ct/now) expires-at)) + show-enabled? (and mcp-enabled? (false? expired?)) + tooltip-id (mf/use-id) @@ -511,14 +506,17 @@ (tr "integrations.mcp-server.status.expired.1")]]]) [:div {:class (stl/css :mcp-server-switch)} - [:> switch* {:label (if mcp-enabled? + [:> switch* {:label (if show-enabled? (tr "integrations.mcp-server.status.enabled") (tr "integrations.mcp-server.status.disabled")) - :default-checked mcp-enabled? + :default-checked show-enabled? :on-change handle-mcp-change}] (when (and (false? mcp-enabled?) (nil? mcp-key)) [:div {:class (stl/css :mcp-server-switch-cover) - :on-click handle-generate-mcp-key}])]]] + :on-click handle-generate-mcp-key}]) + (when (true? expired?) + [:div {:class (stl/css :mcp-server-switch-cover) + :on-click handle-regenerate-mcp-key}])]]] (when (some? mcp-key) [:div {:class (stl/css :mcp-server-key)} @@ -567,7 +565,7 @@ (mf/defc access-tokens-section* {::mf/private true} [] - (let [tokens (mf/deref tokens-ref) + (let [tokens (mf/deref refs/access-tokens) handle-click (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 4d16c79646..674a01549d 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -43,13 +43,9 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [beicon.v2.core :as rx] - [okulary.core :as l] [potok.v2.core :as ptk] [rumext.v2 :as mf])) -(def tokens-ref - (l/derived :access-tokens st/state)) - (mf/defc shortcuts* {::mf/private true} [{:keys [id]}] @@ -749,14 +745,22 @@ (mf/defc mcp-menu* {::mf/private true} [{:keys [on-close]}] - (let [plugins? (features/active-feature? @st/state "plugins/runtime") - - profile (mf/deref refs/profile) - mcp (mf/deref refs/mcp) + (let [plugins? (features/active-feature? @st/state "plugins/runtime") + + profile (mf/deref refs/profile) + mcp (mf/deref refs/mcp) + tokens (mf/deref refs/access-tokens) + + expired? (some->> tokens + (some #(when (= (:type %) "mcp") %)) + :expires-at + (> (ct/now))) mcp-enabled? (true? (-> profile :props :mcp-enabled)) mcp-connected? (= "connected" (get mcp :connection-status)) + show-enabled? (and mcp-enabled? (false? expired?)) + on-nav-to-integrations (mf/use-fn (fn [] @@ -794,7 +798,7 @@ :pos-6 plugins?) :on-close on-close} - (when mcp-enabled? + (when show-enabled? [:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin" :class (stl/css :base-menu-item :submenu-item) :on-click on-toggle-mcp-plugin @@ -809,7 +813,7 @@ :on-click on-nav-to-integrations :on-key-down on-nav-to-integrations-key-down} [:span {:class (stl/css :item-name)} - (if mcp-enabled? + (if show-enabled? (tr "workspace.header.menu.mcp.server.status.enabled") (tr "workspace.header.menu.mcp.server.status.disabled"))]]])) @@ -983,7 +987,7 @@ :class (stl/css :item-arrow)}]]) (when (contains? cf/flags :mcp) - (let [tokens (mf/deref tokens-ref) + (let [tokens (mf/deref refs/access-tokens) expired? (some->> tokens (some #(when (= (:type %) "mcp") %)) :expires-at From ba42cc04b725104010a2d80d04b07ee89d882c28 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 22 Apr 2026 14:17:15 +0000 Subject: [PATCH 13/18] :recycle: Derive v-sizing from values instead of passing as prop Signed-off-by: Andrey Antukh --- .../workspace/sidebar/options/menus/layout_item.cljs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs index ca3e53ee1a..acc1e8db02 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs @@ -539,16 +539,18 @@ [:map [:values schema:layout-item-props-schema] [:applied-tokens [:maybe [:map-of :keyword :string]]] - [:ids [::sm/vec ::sm/uuid]] - [:v-sizing {:optional true} [:maybe [:= :fill]]]]) + [:ids [::sm/vec ::sm/uuid]]]) (mf/defc layout-size-constraints* {::mf/private true ::mf/schema (sm/schema schema:layout-size-constraints)} - [{:keys [values v-sizing ids applied-tokens] :as props}] + [{:keys [values ids applied-tokens] :as props}] (let [token-numeric-inputs (features/use-feature "tokens/numeric-input") + v-sizing + (:layout-item-v-sizing values) + min-w (get values :layout-item-min-w) max-w (get values :layout-item-max-w) @@ -914,5 +916,4 @@ (= v-sizing :fill)) [:> layout-size-constraints* {:ids ids :values values - :applied-tokens applied-tokens - :v-sizing v-sizing}])])])) + :applied-tokens applied-tokens}])])])) From dc8073f92470188e8ee0d6d100f51ad28f7ff66e Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 23 Apr 2026 09:06:10 +0200 Subject: [PATCH 14/18] :whale: Add PENPOT_PUBLIC_URI to penpot-frontend --- docker/images/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 5d3b84d09c..b4ecc2b41d 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -105,7 +105,7 @@ services: # - "traefik.http.routers.penpot-https.tls=true" environment: - << : [*penpot-flags, *penpot-http-body-size] + << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri] penpot-backend: image: "penpotapp/backend:${PENPOT_VERSION:-latest}" From 5f7de04efe0fe5fc1caa7ec42324d2ed014ba30e Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 23 Apr 2026 09:42:40 +0200 Subject: [PATCH 15/18] :ambulance: Fix email blacklisting (#9122) --- backend/src/app/email/blacklist.clj | 14 ++++++-- .../backend_tests/email_blacklist_test.clj | 34 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 backend/test/backend_tests/email_blacklist_test.clj diff --git a/backend/src/app/email/blacklist.clj b/backend/src/app/email/blacklist.clj index ca80afb6c9..a07dfccf91 100644 --- a/backend/src/app/email/blacklist.clj +++ b/backend/src/app/email/blacklist.clj @@ -36,10 +36,18 @@ :cause cause))))) (defn contains? - "Check if email is in the blacklist." + "Check if email is in the blacklist. Also matches subdomains: if + 'somedomain.com' is blacklisted, 'xxx@foo.somedomain.com' will also + be rejected." [{:keys [::email/blacklist]} email] - (let [[_ domain] (str/split email "@" 2)] - (c/contains? blacklist (str/lower domain)))) + (let [[_ domain] (str/split email "@" 2) + parts (str/split (str/lower domain) #"\.")] + (loop [parts parts] + (if (empty? parts) + false + (if (c/contains? blacklist (str/join "." parts)) + true + (recur (rest parts))))))) (defn enabled? "Check if the blacklist is enabled" diff --git a/backend/test/backend_tests/email_blacklist_test.clj b/backend/test/backend_tests/email_blacklist_test.clj new file mode 100644 index 0000000000..5cc043fe32 --- /dev/null +++ b/backend/test/backend_tests/email_blacklist_test.clj @@ -0,0 +1,34 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns backend-tests.email-blacklist-test + (:require + [app.email :as-alias email] + [app.email.blacklist :as blacklist] + [clojure.test :as t])) + +(def ^:private cfg + {::email/blacklist #{"somedomain.com" "spam.net"}}) + +(t/deftest test-exact-domain-match + (t/is (true? (blacklist/contains? cfg "user@somedomain.com"))) + (t/is (true? (blacklist/contains? cfg "user@spam.net"))) + (t/is (false? (blacklist/contains? cfg "user@legit.com")))) + +(t/deftest test-subdomain-match + (t/is (true? (blacklist/contains? cfg "user@sub.somedomain.com"))) + (t/is (true? (blacklist/contains? cfg "user@a.b.somedomain.com"))) + ;; A domain that merely contains the blacklisted string but is not a + ;; subdomain must NOT be rejected. + (t/is (false? (blacklist/contains? cfg "user@notsomedomain.com")))) + +(t/deftest test-case-insensitive + (t/is (true? (blacklist/contains? cfg "user@SOMEDOMAIN.COM"))) + (t/is (true? (blacklist/contains? cfg "user@Sub.SomeDomain.Com")))) + +(t/deftest test-non-blacklisted-domain + (t/is (false? (blacklist/contains? cfg "user@example.com"))) + (t/is (false? (blacklist/contains? cfg "user@sub.legit.com")))) From c6b6b9ce00eea60e738e101ad17abba29f81f1e6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 23 Apr 2026 09:59:11 +0200 Subject: [PATCH 16/18] :paperclip: Update changelog --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 774d17f3de..f9fb9fe1a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,13 @@ # CHANGELOG -## 2.14.4 (Unreleased) +## 2.14.4 ### :bug: Bugs fixed - Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006) +- Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122) +- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927) + ## 2.14.3 From cd417443f63c63ad1bdc0879faa9477960ebab48 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Thu, 23 Apr 2026 18:00:40 +0200 Subject: [PATCH 17/18] :bug: Fix layer hierarchy to match old and new SCSS (#9126) --- .../styles/common/refactor/basic-rules.scss | 2 +- .../resources/styles/common/refactor/z-index.scss | 4 ++-- .../app/main/ui/components/context_menu_a11y.scss | 3 ++- frontend/src/app/main/ui/dashboard/deleted.scss | 12 ++++++------ frontend/src/app/main/ui/dashboard/files.scss | 3 +++ frontend/src/app/main/ui/dashboard/grid.scss | 2 +- frontend/src/app/main/ui/dashboard/projects.scss | 5 ++--- frontend/src/app/main/ui/dashboard/templates.scss | 9 ++++++--- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index c82907a5b6..91068275cc 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -800,7 +800,7 @@ position: absolute; padding: $s-4; border-radius: $br-8; - z-index: $z-index-10; + z-index: $z-index-dropdown; color: var(--title-foreground-color-hover); background-color: var(--menu-background-color); border: $s-2 solid var(--panel-border-color); diff --git a/frontend/resources/styles/common/refactor/z-index.scss b/frontend/resources/styles/common/refactor/z-index.scss index 755b2e9fad..3d36cb37f5 100644 --- a/frontend/resources/styles/common/refactor/z-index.scss +++ b/frontend/resources/styles/common/refactor/z-index.scss @@ -11,5 +11,5 @@ $z-index-4: 4; // context menu $z-index-5: 5; // modal $z-index-10: 10; $z-index-20: 20; -$z-index-modal: 30; // When refactor finish we can reduce this number, -$z-index-alert: 40; // When refactor finish we can reduce this number, +$z-index-modal: 300; +$z-index-dropdown: 400; diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index e0fc29989e..787941b595 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -5,12 +5,13 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/z-index.scss" as *; .context-menu { position: relative; visibility: hidden; opacity: deprecated.$op-0; - z-index: deprecated.$z-index-4; + z-index: var(--z-index-dropdown); &.is-open { position: relative; diff --git a/frontend/src/app/main/ui/dashboard/deleted.scss b/frontend/src/app/main/ui/dashboard/deleted.scss index 8a04eda993..7187633722 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.scss +++ b/frontend/src/app/main/ui/dashboard/deleted.scss @@ -6,11 +6,11 @@ @use "refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-dashboard"; -@use "../ds/typography.scss" as t; -@use "../ds/_borders.scss" as *; -@use "../ds/spacing.scss" as *; -@use "../ds/_sizes.scss" as *; -@use "../ds/z-index.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/spacing.scss" as *; +@use "ds/z-index.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; .dashboard-container { flex: 1 0 0; @@ -51,7 +51,7 @@ padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl); position: sticky; top: 0; - z-index: $z-index-100; + z-index: var(--z-index-panels); } .nav-inside { diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss index 79f3563168..838f8ea78c 100644 --- a/frontend/src/app/main/ui/dashboard/files.scss +++ b/frontend/src/app/main/ui/dashboard/files.scss @@ -6,6 +6,8 @@ @use "refactor/common-refactor.scss" as deprecated; @use "common/refactor/common-dashboard"; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; .dashboard-container { flex: 1 0 0; @@ -13,6 +15,7 @@ overflow-y: auto; width: 100%; border-top: deprecated.$s-1 solid var(--color-background-quaternary); + padding-block-end: var(--sp-xxxl); &.dashboard-projects { user-select: none; diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index 3f4189c729..e1aaef396a 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -17,7 +17,7 @@ $thumbnail-default-height: deprecated.$s-168; // Default width height: 100%; overflow-y: auto; overflow-x: hidden; - padding: 0 deprecated.$s-16; + padding: 0 var(--sp-l) deprecated.$s-16; } .grid-row { diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 7df6a0f9c9..a37575c38e 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -19,16 +19,15 @@ margin-inline-end: var(--sp-l); border-block-start: $b-1 solid var(--panel-border-color); overflow-y: auto; - padding-block-end: var(--sp-xxxl); } .dashboard-projects { user-select: none; - block-size: calc(100vh - px2rem(64)); + block-size: calc(100vh - px2rem(80)); } .with-team-hero { - block-size: calc(100vh - px2rem(280)); + block-size: calc(100vh - px2rem(360)); } .dashboard-shared { diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index 8a5ab660c0..5d58ac4fea 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -4,10 +4,11 @@ // // Copyright (c) KALEIDOS INC -@use "ds/_borders.scss" as *; -@use "ds/_utils.scss" as *; -@use "ds/_sizes.scss" as *; @use "ds/typography.scss" as t; +@use "ds/z-index.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; .dashboard-templates-section { background-color: var(--color-background-tertiary); @@ -26,6 +27,8 @@ transition: bottom 300ms; width: calc(100% - $sz-12); pointer-events: none; + z-index: var(--z-index-set); + &.collapsed { inset-block-end: calc(-1 * px2rem(228)); background-color: transparent; From 2d5e50f35254f38c98303716e08097e051a5f1c5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 24 Apr 2026 08:17:32 +0200 Subject: [PATCH 18/18] :arrow_up: Update root repo deps --- package.json | 6 +- pnpm-lock.yaml | 182 ++++++++++++++++++++++++------------------------- 2 files changed, 94 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index fbb4c5d92f..d2d6a9f5a8 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "fmt": "./scripts/fmt" }, "devDependencies": { - "@github/copilot": "^1.0.21", - "@types/node": "^25.5.2", + "@github/copilot": "^1.0.35", + "@types/node": "^25.6.0", "esbuild": "^0.28.0", - "opencode-ai": "^1.14.19" + "opencode-ai": "^1.14.22" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32b4fa3382..cf5a098662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,17 +9,17 @@ importers: .: devDependencies: '@github/copilot': - specifier: ^1.0.21 - version: 1.0.21 + specifier: ^1.0.35 + version: 1.0.35 '@types/node': - specifier: ^25.5.2 - version: 25.5.2 + specifier: ^25.6.0 + version: 25.6.0 esbuild: specifier: ^0.28.0 version: 0.28.0 opencode-ai: - specifier: ^1.14.19 - version: 1.14.19 + specifier: ^1.14.22 + version: 1.14.22 packages: @@ -179,120 +179,120 @@ packages: cpu: [x64] os: [win32] - '@github/copilot-darwin-arm64@1.0.21': - resolution: {integrity: sha512-aB+s9ldTwcyCOYmzjcQ4SknV6g81z92T8aUJEJZBwOXOTBeWKAJtk16ooAKangZgdwuLgO3or1JUjx1FJAm5nQ==} + '@github/copilot-darwin-arm64@1.0.35': + resolution: {integrity: sha512-NNZE0TOz0HOlv7eqlh6EcQbNkhtnIHReBLieW6pfDUUTKkgsqbUu1MOitF8m+LUQk3ml1T0MQ5MOfad1HSa/MQ==} cpu: [arm64] os: [darwin] hasBin: true - '@github/copilot-darwin-x64@1.0.21': - resolution: {integrity: sha512-aNad81DOGuGShmaiFNIxBUSZLwte0dXmDYkGfAF9WJIgY4qP4A8CPWFoNr8//gY+4CwaIf9V+f/OC6k2BdECbw==} + '@github/copilot-darwin-x64@1.0.35': + resolution: {integrity: sha512-XCv/mfdv0rnrtrNVOluio/N/kyCge0uG2hghvtlgO/+z6EjvzFygkpXXS1gVxiXhWc3lX232cTXQU3zklC/8Ng==} cpu: [x64] os: [darwin] hasBin: true - '@github/copilot-linux-arm64@1.0.21': - resolution: {integrity: sha512-FL0NsCnHax4czHVv1S8iBqPLGZDhZ28N3+6nT29xWGhmjBWTkIofxLThKUPcyyMsfPTTxIlrdwWa8qQc5z2Q+g==} + '@github/copilot-linux-arm64@1.0.35': + resolution: {integrity: sha512-mbaadATfJPzmXq2SD1TWocIG/GobcYC6OvNFhCG8UXMsiXY5cevhszl5ujuayhPJBxS77Yj5uvIFjNQ1Kf5V8Q==} cpu: [arm64] os: [linux] hasBin: true - '@github/copilot-linux-x64@1.0.21': - resolution: {integrity: sha512-S7pWVI16hesZtxYbIyfw+MHZpc5ESoGKUVr5Y+lZJNaM2340gJGPQzQwSpvKIRMLHRKI2hXLwciAnYeMFxE/Tg==} + '@github/copilot-linux-x64@1.0.35': + resolution: {integrity: sha512-NrZ0VjztdBbJ5qAmuUtuKsWkimOaqzjDV+ZGUv1FxSxoys40kiiakQ5WbnMFDzaIFaf47zDi++6ixgQzq7Jk5A==} cpu: [x64] os: [linux] hasBin: true - '@github/copilot-win32-arm64@1.0.21': - resolution: {integrity: sha512-a9qc2Ku+XbyBkXCclbIvBbIVnECACTIWnPctmXWsQeSdeapGxgfHGux7y8hAFV5j6+nhCm6cnyEMS3rkZjAhdA==} + '@github/copilot-win32-arm64@1.0.35': + resolution: {integrity: sha512-KQN7Q7+oPyglmvUEiMp6SYWjl30VSu91T0dUpNHbUs/xRM3qgnCymLPPUyBZGWHog/FueUAsRkhisMHWQVnO+g==} cpu: [arm64] os: [win32] hasBin: true - '@github/copilot-win32-x64@1.0.21': - resolution: {integrity: sha512-9klu+7NQ6tEyb8sibb0rsbimBivDrnNltZho10Bgbf1wh3o+erTjffXDjW9Zkyaw8lZA9Fz8bqhVkKntZq58Lg==} + '@github/copilot-win32-x64@1.0.35': + resolution: {integrity: sha512-J0XhXO2FmlFr8pGa970xEd4tr1rqFiZxoaPW5WvkJYZoZUHbBhFcGasp5/yEeJ71b3vI4PHm/mSZZebD3ALMKQ==} cpu: [x64] os: [win32] hasBin: true - '@github/copilot@1.0.21': - resolution: {integrity: sha512-P+nORjNKAtl92jYCG6Qr1Rsw2JoyScgeQSkIR6O2WB37WS5JVdA4ax1WVualMbfuc9V58CPHX6fwyNpkI89FkQ==} + '@github/copilot@1.0.35': + resolution: {integrity: sha512-O1nUy8DXOTE+v86b/FTkyu09EMrDy+vj+2rhmUOcmsXGe0RE5ECyESsasUTUoHK/CSgAExFTziNxbubUoiMMfg==} hasBin: true - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true - opencode-ai@1.14.19: - resolution: {integrity: sha512-67h56qYcJivd2U9VK8LJvyMBCc3ZE3HcJ/qL4YtaidSnjEumy4SxO+HHlDISsLase7TUQ0w1nULOAibdZxGzbQ==} + opencode-ai@1.14.22: + resolution: {integrity: sha512-J+q1Ehlfg7SSXw2aIY8Mb47FHhPTN8IciKNt0/D+H/brO8RWLe67WjFzxhh/z9SSad9wPcCiLRGAc/iAn8W8wA==} hasBin: true - opencode-darwin-arm64@1.14.19: - resolution: {integrity: sha512-gjJ97dTiBbCas3Y7K6KQMQm5/KNIBOM9+10KZHqNr2bQfn0N09O977ZkXoX6IVBBE2632Ahaa71d4pzLKN9ULw==} + opencode-darwin-arm64@1.14.22: + resolution: {integrity: sha512-h9FjzNoDRsuJD0EEg535P9ul5TyrWovwx591VmuG8fp9d4PoSrAN1O3Zi07GJjkrYyrB8g3c+x5whDqJCz+qog==} cpu: [arm64] os: [darwin] - opencode-darwin-x64-baseline@1.14.19: - resolution: {integrity: sha512-UuwLpa511h7qQ+rTmOUmsHch/N4NBQT64dzg+iLWnR5yoR/81inENpbwxiS7hXpVdzCUGo/YnxI1u6SIBoMlTQ==} + opencode-darwin-x64-baseline@1.14.22: + resolution: {integrity: sha512-GgfP0wSm9/I+j3shOxfeA++7yZpXS6Y1Vis258nEFoRS9Xfv3YlHom7c/8BR9rYqeUE/+rrijP7PrGWGl+IHBw==} cpu: [x64] os: [darwin] - opencode-darwin-x64@1.14.19: - resolution: {integrity: sha512-uksrjOtWI7Ob5JvjZBSsrKuy3JVF9d89oYZYfWS5m8ordNyv1Nob39MXJXizv85ozsXjSb0rqjpJurnJw8K+tQ==} + opencode-darwin-x64@1.14.22: + resolution: {integrity: sha512-cyKRo22sxDwu4ITOlENwXaqVM9kMGndwSaAd95gz1Rmz5NYMShUO/8eckrD2MhS2wm+QvKw9XkRVWVHWQlZw3Q==} cpu: [x64] os: [darwin] - opencode-linux-arm64-musl@1.14.19: - resolution: {integrity: sha512-R1BJuBGWHfBxfKvIA/Hb4nhYaJgCKl1B+mAGNydu+z0CLtGtwU8r+kQWF/G2N0y8Vx6Y6DRfJiv1X0eZEfD1BQ==} + opencode-linux-arm64-musl@1.14.22: + resolution: {integrity: sha512-DtSd5tbGk6R5+hGhqViSvbY8ICf+u4oVQhfvCAplQCb1UEwYVc0+oAF6PimFJ+o8i8L6x14O0rry0NaRzZ0CzA==} cpu: [arm64] os: [linux] - opencode-linux-arm64@1.14.19: - resolution: {integrity: sha512-Bo+aZOppLF366mgGfK0CnIcAVy1EmsrBv93eot1CmPSN1oeud07GpGdn3Bjl5f6KuBx1io6JsvjQyHno+MH5AA==} + opencode-linux-arm64@1.14.22: + resolution: {integrity: sha512-ohK4LkkGvzB4ptr0nqDOVi2JEJMLROfy1s2U2A4Qrh+1Y0QimgH2b5VgTm+BjA3bC2Hm8Yf/IfkitqlUnCp7YA==} cpu: [arm64] os: [linux] - opencode-linux-x64-baseline-musl@1.14.19: - resolution: {integrity: sha512-+K8MuGoHugtUec4P/nKcTwZFUipHfW7oPpwlIoPiAQou3bNFTzzP6rslbzzNwjXlQRsUw9GAtuIPDOCL6CkgDg==} + opencode-linux-x64-baseline-musl@1.14.22: + resolution: {integrity: sha512-oZffotEbGXbA38Y0Dmj7IVq0ATl3nKbP8j91Z0zR5kBEBykOqExJIyc9pZpModgfPf86k98XBsRHiVLK4u9ARw==} cpu: [x64] os: [linux] - opencode-linux-x64-baseline@1.14.19: - resolution: {integrity: sha512-KuvITzg4iK0hdIjpNZepwu3bLZ/dUZDI6BwCoV4w//VEP1j3UfDyeS3vWghKcQLd2T1+yybuEMM/3RXcwm/lGQ==} + opencode-linux-x64-baseline@1.14.22: + resolution: {integrity: sha512-J67YAIWr3E03o9e6wNaPEqBo+9FcPKf5CzjIUSb8yNDyobWON1HHihcuu0hCJ6wF9J9awmlp2/4mO1HOoCo3QQ==} cpu: [x64] os: [linux] - opencode-linux-x64-musl@1.14.19: - resolution: {integrity: sha512-0qe2+X76UJdrCdhdlJyfubMC4tveHAVxjSmPq7g9Zm95heBeJdcQDCLeyQk/lGgeXgsZzVPfLmyTWNtBvCZYFQ==} + opencode-linux-x64-musl@1.14.22: + resolution: {integrity: sha512-r+QnqwR/OPmMm197Kb8VLD9mkZGFXz4m5QCZFxOAL34k8AhQZqn3d2mx2bfrMBVfoSiSVxa3jEjZEbNNFGlICQ==} cpu: [x64] os: [linux] - opencode-linux-x64@1.14.19: - resolution: {integrity: sha512-2GljfL7BeG4xALBJVRwaAGCM/dzYF5aQf6bfLTKsQIl6QpLUguYSF+fkStBHLeehyqbDP5MtiEEuXjC0+mecjA==} + opencode-linux-x64@1.14.22: + resolution: {integrity: sha512-MSUaO/Cvfb8DFRYETVrVeCnKtoIfgLflyB+O8xQOkVtjMKJ41M+1dFSMyZ3LQa2Vfp5tDskyMhj7eUxvT/owgQ==} cpu: [x64] os: [linux] - opencode-windows-arm64@1.14.19: - resolution: {integrity: sha512-8/vRHe5tHexikfPceLmpjsQiEhuDTOSCSlEmP4s0Yq3UAkVaDAxpiWq7Bx4g8hjr1gzfXD9vbjV+WHq/BtMC/w==} + opencode-windows-arm64@1.14.22: + resolution: {integrity: sha512-8grcxLSf9BD9Bt38MIxXfkI6aOFophVgM0US5r8nAUdVU78/8TS9Flnn6D39GM5RmxzqGWMl1u10vMFrBtMwPA==} cpu: [arm64] os: [win32] - opencode-windows-x64-baseline@1.14.19: - resolution: {integrity: sha512-Z8imEJK/srE/r1fr7oNLvpLTeRJQyuL7vsbXvCt3T7j2Ew9BOZ7RuYa8EE0R6bNqQ+MLhBGPiAG7NWc++MgK8Q==} + opencode-windows-x64-baseline@1.14.22: + resolution: {integrity: sha512-R/o36LpmQmbv/tL2pkcmApn6030z/1oJIYmjDkW5a4K5MXmV7aq+jWrH5p6iYKp9fo9L8oCtOp/rELMBqDS3UA==} cpu: [x64] os: [win32] - opencode-windows-x64@1.14.19: - resolution: {integrity: sha512-/TqGN91WiUzx7IPMPwmpMIzRixi5TMjaBcC9FH1TgD7DCqKnP6pokvu+ak0C9xwA4wKorE9PoZzeRt/+c3rDCQ==} + opencode-windows-x64@1.14.22: + resolution: {integrity: sha512-jVbZ4VA5b5MF2QhWQOE1VYBKdBE0v/ZebFjwzs6Vieazfgr6OFnGSHVP5WJbU/r6zDssbTBzzpnFxo0IY1SQWw==} cpu: [x64] os: [win32] - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} snapshots: @@ -374,36 +374,36 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@github/copilot-darwin-arm64@1.0.21': + '@github/copilot-darwin-arm64@1.0.35': optional: true - '@github/copilot-darwin-x64@1.0.21': + '@github/copilot-darwin-x64@1.0.35': optional: true - '@github/copilot-linux-arm64@1.0.21': + '@github/copilot-linux-arm64@1.0.35': optional: true - '@github/copilot-linux-x64@1.0.21': + '@github/copilot-linux-x64@1.0.35': optional: true - '@github/copilot-win32-arm64@1.0.21': + '@github/copilot-win32-arm64@1.0.35': optional: true - '@github/copilot-win32-x64@1.0.21': + '@github/copilot-win32-x64@1.0.35': optional: true - '@github/copilot@1.0.21': + '@github/copilot@1.0.35': optionalDependencies: - '@github/copilot-darwin-arm64': 1.0.21 - '@github/copilot-darwin-x64': 1.0.21 - '@github/copilot-linux-arm64': 1.0.21 - '@github/copilot-linux-x64': 1.0.21 - '@github/copilot-win32-arm64': 1.0.21 - '@github/copilot-win32-x64': 1.0.21 + '@github/copilot-darwin-arm64': 1.0.35 + '@github/copilot-darwin-x64': 1.0.35 + '@github/copilot-linux-arm64': 1.0.35 + '@github/copilot-linux-x64': 1.0.35 + '@github/copilot-win32-arm64': 1.0.35 + '@github/copilot-win32-x64': 1.0.35 - '@types/node@25.5.2': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 esbuild@0.28.0: optionalDependencies: @@ -434,55 +434,55 @@ snapshots: '@esbuild/win32-ia32': 0.28.0 '@esbuild/win32-x64': 0.28.0 - opencode-ai@1.14.19: + opencode-ai@1.14.22: optionalDependencies: - opencode-darwin-arm64: 1.14.19 - opencode-darwin-x64: 1.14.19 - opencode-darwin-x64-baseline: 1.14.19 - opencode-linux-arm64: 1.14.19 - opencode-linux-arm64-musl: 1.14.19 - opencode-linux-x64: 1.14.19 - opencode-linux-x64-baseline: 1.14.19 - opencode-linux-x64-baseline-musl: 1.14.19 - opencode-linux-x64-musl: 1.14.19 - opencode-windows-arm64: 1.14.19 - opencode-windows-x64: 1.14.19 - opencode-windows-x64-baseline: 1.14.19 + opencode-darwin-arm64: 1.14.22 + opencode-darwin-x64: 1.14.22 + opencode-darwin-x64-baseline: 1.14.22 + opencode-linux-arm64: 1.14.22 + opencode-linux-arm64-musl: 1.14.22 + opencode-linux-x64: 1.14.22 + opencode-linux-x64-baseline: 1.14.22 + opencode-linux-x64-baseline-musl: 1.14.22 + opencode-linux-x64-musl: 1.14.22 + opencode-windows-arm64: 1.14.22 + opencode-windows-x64: 1.14.22 + opencode-windows-x64-baseline: 1.14.22 - opencode-darwin-arm64@1.14.19: + opencode-darwin-arm64@1.14.22: optional: true - opencode-darwin-x64-baseline@1.14.19: + opencode-darwin-x64-baseline@1.14.22: optional: true - opencode-darwin-x64@1.14.19: + opencode-darwin-x64@1.14.22: optional: true - opencode-linux-arm64-musl@1.14.19: + opencode-linux-arm64-musl@1.14.22: optional: true - opencode-linux-arm64@1.14.19: + opencode-linux-arm64@1.14.22: optional: true - opencode-linux-x64-baseline-musl@1.14.19: + opencode-linux-x64-baseline-musl@1.14.22: optional: true - opencode-linux-x64-baseline@1.14.19: + opencode-linux-x64-baseline@1.14.22: optional: true - opencode-linux-x64-musl@1.14.19: + opencode-linux-x64-musl@1.14.22: optional: true - opencode-linux-x64@1.14.19: + opencode-linux-x64@1.14.22: optional: true - opencode-windows-arm64@1.14.19: + opencode-windows-arm64@1.14.22: optional: true - opencode-windows-x64-baseline@1.14.19: + opencode-windows-x64-baseline@1.14.22: optional: true - opencode-windows-x64@1.14.19: + opencode-windows-x64@1.14.22: optional: true - undici-types@7.18.2: {} + undici-types@7.19.2: {}