From c8675c5b7eb6679357bfe262620f1ea2effcb4df Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Apr 2026 17:00:52 +0200 Subject: [PATCH 1/8] :recycle: Normalize newsletter-updates checbox on different register flows (#8839) * :sparkles: Add newsletter opt-in checkbox to registration validation form Add accept-newsletter-updates support through the full registration token flow. The newsletter checkbox is now available on the registration validation form, allowing users to opt-in during the email verification step. Backend changes: - Refactor prepare-register to consolidate UTM params and newsletter preference into props at token creation time - Add accept-newsletter-updates to prepare-register-profile and register-profile schemas - Handle newsletter-updates in register-profile by updating token claims props on second step Frontend changes: - Add newsletter-options component to register-validate-form - Add accept-newsletter-updates to validation schema - Fix subscription finalize/error handling in register form Signed-off-by: Andrey Antukh * :recycle: Refactor auth register components to modern style Migrate all components in app.main.ui.auth.register and app.main.ui.auth.login/demo-warning to use the modern * suffix convention, removing deprecated ::mf/props :obj metadata and updating all invocations from [:& name] to [:> name*] syntax. Components updated: - terms-and-privacy -> terms-and-privacy* - register-form -> register-form* - register-methods -> register-methods* - register-page -> register-page* - register-success-page -> register-success-page* - terms-register -> terms-register* - register-validate-form -> register-validate-form* - register-validate-page -> register-validate-page* - demo-warning -> demo-warning* Also remove unused old context-notification import in login.cljs. Signed-off-by: Andrey Antukh * :fire: Remove unused onboarding-newsletter component The newsletter opt-in is now handled directly in the registration form via the newsletter-options* component, making the standalone onboarding-newsletter modal obsolete. Signed-off-by: Andrey Antukh * :bug: Fix register test for UTM params to use prepare-register step UTM params are now extracted and stored in token props during the prepare-register step, not at register-profile time. Move utm_campaign and mtm_campaign from the register-profile call to the prepare-register-profile call in the test. Signed-off-by: Andrey Antukh --------- Signed-off-by: Andrey Antukh --- backend/src/app/auth/oidc.clj | 2 +- backend/src/app/rpc/commands/auth.clj | 24 ++-- .../test/backend_tests/rpc_profile_test.clj | 12 +- frontend/src/app/main/ui.cljs | 10 -- frontend/src/app/main/ui/auth.cljs | 31 ++--- frontend/src/app/main/ui/auth/login.cljs | 8 +- frontend/src/app/main/ui/auth/register.cljs | 51 ++++----- .../app/main/ui/onboarding/newsletter.cljs | 106 ------------------ .../app/main/ui/onboarding/newsletter.scss | 76 ------------- frontend/src/app/main/ui/static.cljs | 8 +- frontend/src/app/main/ui/viewer/login.cljs | 12 +- 11 files changed, 75 insertions(+), 265 deletions(-) delete mode 100644 frontend/src/app/main/ui/onboarding/newsletter.cljs delete mode 100644 frontend/src/app/main/ui/onboarding/newsletter.scss diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 7668a49b99..fa819c5e0c 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -423,7 +423,7 @@ (defn- qualify-prop-key [provider k] - (keyword (:type provider) (name k))) + (keyword (:type provider) (-> k name str/kebab))) (defn- qualify-props [provider props] diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index fb5db45ef7..f3466f6d21 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -253,12 +253,15 @@ :hint "email has complaint reports"))) (defn prepare-register - [{:keys [::db/pool] :as cfg} {:keys [fullname email accept-newsletter-updates] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [fullname email] :as params}] (validate-register-attempt! cfg params) (let [email (profile/clean-email email) profile (profile/get-profile-by-email pool email) + props (-> (audit/extract-utm-params params) + (cond-> (:accept-newsletter-updates params) + (assoc :newsletter-updates true))) params {:email email :fullname fullname :password (:password params) @@ -267,13 +270,12 @@ :iss :prepared-register :profile-id (:id profile) :exp (ct/in-future {:days 7}) - :props {:newsletter-updates (or accept-newsletter-updates false)}} - + :props props} params (d/without-nils params) token (tokens/generate cfg params)] - (with-meta {:token token} - {::audit/profile-id uuid/zero}))) + (-> {:token token} + (with-meta {::audit/profile-id uuid/zero})))) (def schema:prepare-register-profile [:map {:title "prepare-register-profile"} @@ -281,6 +283,7 @@ [:email ::sm/email] [:password schema:password] [:create-welcome-file {:optional true} :boolean] + [:accept-newsletter-updates {:optional true} :boolean] [:invitation-token {:optional true} schema:token]]) (sv/defmethod ::prepare-register-profile @@ -317,8 +320,7 @@ attrs (all the other attrs are filled with default values)." [{:keys [::db/conn] :as cfg} {:keys [email] :as params}] (let [id (or (:id params) (uuid/next)) - props (-> (audit/extract-utm-params params) - (merge (:props params)) + props (-> (:props params) (merge {:viewed-tutorial? false :viewed-walkthrough? false :nudge {:big 10 :small 1} @@ -369,7 +371,6 @@ :cause cause) (throw cause)))))) - (defn create-profile-rels [conn {:keys [id] :as profile}] (let [features (cfeat/get-enabled-features cf/flags) @@ -409,7 +410,9 @@ (defn register-profile [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}] (let [claims (tokens/verify cfg {:token token :iss :prepared-register}) - params (into claims params) + params (cond-> claims + (:accept-newsletter-updates params) + (update :props assoc :newsletter-updates true)) profile (if-let [profile-id (:profile-id claims)] (profile/get-profile conn profile-id) @@ -524,7 +527,8 @@ (def schema:register-profile [:map {:title "register-profile"} - [:token schema:token]]) + [:token schema:token] + [:accept-newsletter-updates {:optional true} :boolean]]) (sv/defmethod ::register-profile {::rpc/auth false diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index ec18cc10b2..1cdf16a99f 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -380,7 +380,9 @@ (let [data {::th/type :prepare-register-profile :email "user@example.com" :fullname "foobar" - :password "foobar"} + :password "foobar" + :utm_campaign "utma" + :mtm_campaign "mtma"} out (th/command! data) token (get-in out [:result :token])] (t/is (string? token)) @@ -396,11 +398,9 @@ ;; try correct register (let [data {::th/type :register-profile - :token token - :utm_campaign "utma" - :mtm_campaign "mtma"}] - (let [{:keys [result error]} (th/command! data)] - (t/is (nil? error)))) + :token token} + out (th/command! data)] + (t/is (nil? (:error out)))) (let [profile (some-> (th/db-get :profile {:email "user@example.com"}) (profile/decode-row))] diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index a247a982a8..3b00fe0d50 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -24,7 +24,6 @@ [app.main.ui.exports.files] [app.main.ui.frame-preview :as frame-preview] [app.main.ui.notifications :as notifications] - [app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]] [app.main.ui.onboarding.questions :refer [questions-modal]] [app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]] [app.main.ui.releases :refer [release-notes-modal]] @@ -160,12 +159,6 @@ (not (:onboarding-viewed props)) (not (contains? props :onboarding-questions))) - show-newsletter-modal? - (and (contains? cf/flags :onboarding) - (not (:onboarding-viewed props)) - (not (contains? props :newsletter-updates)) - (contains? props :onboarding-questions)) - show-team-modal? (and (contains? cf/flags :onboarding) (not (:onboarding-viewed props)) @@ -242,9 +235,6 @@ show-question-modal? [:& questions-modal] - show-newsletter-modal? - [:& onboarding-newsletter] - show-team-modal? [:& onboarding-team-modal {:go-to-team true}] diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index ef4fa951a7..724229caea 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -13,7 +13,7 @@ [app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-page register-success-page register-validate-page terms-register]] + [app.main.ui.auth.register :refer [register-page* register-success-page* register-validate-page* terms-register*]] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -21,14 +21,19 @@ (mf/defc auth* [{:keys [route]}] - (let [section (dm/get-in route [:data :name]) - is-register (or - (= section :auth-register) - (= section :auth-register-validate) - (= section :register-validate-page) - (= section :auth-register-success)) - params (:query-params route) - error (:error params)] + (let [section + (dm/get-in route [:data :name]) + + is-register + (or (= section :auth-register) + (= section :auth-register-validate) + (= section :register-validate-page) + (= section :auth-register-success)) + params + (:query-params route) + + error + (:error params)] (mf/with-effect [] (dom/set-html-title (tr "title.default"))) @@ -49,13 +54,13 @@ (case section :auth-register - [:& register-page {:params params}] + [:> register-page* {:params params}] :auth-register-success - [:& register-success-page {:params params}] + [:> register-success-page* {:params params}] :auth-register-validate - [:& register-validate-page {:params params}] + [:> register-validate-page* {:params params}] :auth-login [:& login-page {:params params}] @@ -67,7 +72,7 @@ [:& recovery-page {:params params}]) (when (= section :auth-register) - [:& terms-register])]])) + [:> terms-register*])]])) (mf/defc auth-page* diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 63becbc0a6..fd601978b8 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -20,7 +20,6 @@ [app.main.ui.components.link :as lk] [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] [app.main.ui.icons :as deprecated-icon] - [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.storage :as s] @@ -34,10 +33,9 @@ :login-with-gitlab :login-with-oidc])) -(mf/defc demo-warning - {::mf/props :obj} +(mf/defc demo-warning* [] - [:& context-notification + [:> context-notification* {:level :warning :content (tr "auth.demo-warning")}]) @@ -274,7 +272,7 @@ (tr "auth.login-tagline")] (when (contains? cf/flags :demo-warning) - [:& demo-warning]) + [:> demo-warning*]) [:> login-dialog* {:params params}] diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 45ec87dd2e..3bd3fdf564 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -40,9 +40,8 @@ :default-checked false :label updates-label}]])) -(mf/defc terms-and-privacy - {::mf/props :obj - ::mf/private true} +(mf/defc terms-and-privacy* + {::mf/private true} [] (let [terms-label (mf/html @@ -70,8 +69,7 @@ [:accept-newsletter-updates {:optional true} :boolean] [:token {:optional true} ::sm/text]]) -(mf/defc register-form - {::mf/props :obj} +(mf/defc register-form* [{:keys [params on-success-callback]}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) form (fm/use-form :schema schema:register-form @@ -150,8 +148,7 @@ (assoc :create-welcome-file true))] (->> (rp/cmd! :prepare-register-profile cdata) - (rx/finalize #(reset! submitted? false)) - (rx/subs! on-register-profile on-error)))))] + (rx/subs! on-register-profile on-error #(reset! submitted? false))))))] [:& fm/form {:on-submit on-submit :form form} [:div {:class (stl/css :fields-row)} @@ -177,7 +174,7 @@ :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - [:& terms-and-privacy]) + [:> terms-and-privacy*]) [:> newsletter-options*] @@ -187,8 +184,7 @@ :data-testid "register-form-submit" :class (stl/css :register-btn)}]])) -(mf/defc register-methods - {::mf/props :obj} +(mf/defc register-methods* [{:keys [params hide-separator on-success-callback]}] [:* (when login/show-sso-login-buttons? @@ -196,19 +192,18 @@ (when (or login/show-sso-login-buttons? (false? hide-separator)) [:hr {:class (stl/css :separator)}]) (when (contains? cf/flags :login-with-password) - [:& register-form {:params params :on-success-callback on-success-callback}])]) + [:> register-form* {:params params :on-success-callback on-success-callback}])]) -(mf/defc register-page - {::mf/props :obj} +(mf/defc register-page* [{:keys [params]}] [:div {:class (stl/css :auth-form-wrapper :register-form)} [:h1 {:class (stl/css :auth-title) :data-testid "registration-title"} (tr "auth.register-title")] (when (contains? cf/flags :demo-warning) - [:& login/demo-warning]) + [:> login/demo-warning*]) - [:& register-methods {:params params}] + [:> register-methods* {:params params}] [:div {:class (stl/css :links)} [:div {:class (stl/css :account)} @@ -228,8 +223,7 @@ ;; --- PAGE: register success page -(mf/defc register-success-page - {::mf/props :obj} +(mf/defc register-success-page* [{:keys [params]}] (let [email (or (:email params) (::email storage/user))] [:div {:class (stl/css :auth-form-wrapper :register-success)} @@ -239,8 +233,7 @@ [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]] [:div {:class (stl/css :notification-text-email)} email]])) - -(mf/defc terms-register +(mf/defc terms-register* [] (let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri) show-terms? (some? cf/terms-of-service-uri) @@ -267,13 +260,14 @@ [:token ::sm/text] [:fullname [::sm/text {:max 250}]] [:accept-terms-and-privacy {:optional (not (contains? cf/flags :terms-and-privacy-checkbox))} - [:and :boolean [:= true]]]]) + [:and :boolean [:= true]]] + [:accept-newsletter-updates {:optional true} :boolean]]) -(mf/defc register-validate-form - {::mf/props :obj - ::mf/private true} +(mf/defc register-validate-form* + {::mf/private true} [{:keys [params on-success-callback]}] - (let [form (fm/use-form :schema schema:register-validate-form :initial params) + (let [form + (fm/use-form :schema schema:register-validate-form :initial params) submitted? (mf/use-state false) @@ -331,7 +325,9 @@ :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - [:& terms-and-privacy]) + [:> terms-and-privacy*]) + + [:> newsletter-options*] [:> fm/submit-button* {:label (tr "auth.register-submit") @@ -339,8 +335,7 @@ :class (stl/css :register-btn)}]])) -(mf/defc register-validate-page - {::mf/props :obj} +(mf/defc register-validate-page* [{:keys [params]}] [:div {:class (stl/css :auth-form-wrapper :register-form)} @@ -350,7 +345,7 @@ :data-testid "register-title"} (tr "auth.register-account-title")] [:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]] - [:& register-validate-form {:params params}] + [:> register-validate-form* {:params params}] [:div {:class (stl/css :links)} [:div {:class (stl/css :go-back)} diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs deleted file mode 100644 index 3f5ee18557..0000000000 --- a/frontend/src/app/main/ui/onboarding/newsletter.cljs +++ /dev/null @@ -1,106 +0,0 @@ -;; 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 app.main.ui.onboarding.newsletter - (:require-macros [app.main.style :as stl]) - (:require - [app.main.data.event :as-alias ev] - [app.main.data.notifications :as ntf] - [app.main.data.profile :as du] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.icons :as deprecated-icon] - [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr]] - [potok.v2.core :as ptk] - [rumext.v2 :as mf])) - -(mf/defc onboarding-newsletter - [] - (let [state* (mf/use-state #(do {:newsletter-updates false - :newsletter-news false})) - state (deref state*) - team (mf/deref refs/team) - - on-change - (mf/use-fn - (fn [event] - (let [attr (-> (dom/get-current-target event) - (dom/get-data "attr") - (keyword))] - (swap! state* update attr not)))) - - on-next - (mf/use-fn - (mf/deps state team) - (fn [] - (when (or (:newsletter-updates state) - (:newsletter-news state)) - (st/emit! (ntf/success (tr "onboarding.newsletter.acceptance-message")))) - - (let [params (-> state - (assoc ::ev/name "onboarding-step") - (assoc :label "newsletter:subscriptions") - (assoc :step 6))] - (st/emit! (ptk/data-event ::ev/event params) - (du/update-profile-props - (cond-> state - (not (:is-default team)) - (assoc :onboarding-viewed true)))))))] - - [:div {:class (stl/css-case - :modal-overlay true)} - - [:div.animated.fadeInDown {:class (stl/css :modal-container)} - [:div {:class (stl/css :modal-left)} - [:img {:src "images/deco-newsletter.png" - :border "0"}]] - - [:div {:class (stl/css :modal-right)} - [:h2 {:class (stl/css :modal-title) - :data-testid "onboarding-newsletter-title"} - (tr "onboarding.newsletter.title")] - - [:p {:class (stl/css :modal-text)} - (tr "onboarding-v2.newsletter.desc")] - - [:div {:class (stl/css :newsletter-options)} - [:div {:class (stl/css :input-wrapper)} - [:label {:for "newsletter-updates"} - [:span {:class (stl/css-case :global/checked (:newsletter-updates state))} - deprecated-icon/status-tick] - - (tr "onboarding-v2.newsletter.updates") - [:input {:type "checkbox" - :id "newsletter-updates" - :data-attr "newsletter-updates" - :value (:newsletter-updates state) - :on-change on-change}]]] - - [:div {:class (stl/css :input-wrapper)} - [:label {:for "newsletter-news"} - [:span {:class (stl/css-case :global/checked (:newsletter-news state))} - deprecated-icon/status-tick] - - (tr "onboarding-v2.newsletter.news") - [:input {:type "checkbox" - :id "newsletter-news" - :data-attr "newsletter-news" - :value (:newsletter-news state) - :on-change on-change}]]]] - - [:p {:class (stl/css :modal-text)} - (tr "onboarding-v2.newsletter.privacy1") - [:a {:class (stl/css :modal-link) - :target "_blank" - :href "https://penpot.app/privacy"} - (tr "onboarding.newsletter.policy")]] - [:p {:class (stl/css :modal-text)} - (tr "onboarding-v2.newsletter.privacy2")] - - [:button {:on-click on-next - :class (stl/css :accept-btn)} - (tr "labels.continue")]]]])) diff --git a/frontend/src/app/main/ui/onboarding/newsletter.scss b/frontend/src/app/main/ui/onboarding/newsletter.scss deleted file mode 100644 index 34abf708bd..0000000000 --- a/frontend/src/app/main/ui/onboarding/newsletter.scss +++ /dev/null @@ -1,76 +0,0 @@ -// 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 - -@use "refactor/common-refactor.scss" as deprecated; - -.modal-overlay { - @extend .modal-overlay-base; -} - -.modal-container { - @extend .modal-container-base; - position: relative; - display: grid; - grid-template-columns: auto auto; - gap: deprecated.$s-32; - padding-inline: deprecated.$s-100; - padding-block-start: deprecated.$s-100; - padding-block-end: deprecated.$s-72; - margin: 0; - width: deprecated.$s-960; - height: deprecated.$s-632; - max-width: deprecated.$s-960; - max-height: deprecated.$s-632; -} - -.modal-left { - width: deprecated.$s-172; - margin-block-end: deprecated.$s-64; - img { - width: deprecated.$s-172; - border-radius: deprecated.$br-8 0 0 deprecated.$br-8; - } -} - -.modal-right { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: deprecated.$s-40 auto auto auto auto deprecated.$s-32; - gap: deprecated.$s-24; - position: relative; -} - -.modal-title { - @include deprecated.bigTitleTipography; - color: var(--modal-title-foreground-color); -} - -.modal-text { - @include deprecated.bodyLargeTypography; - color: var(--modal-text-foreground-color); - margin: 0; -} - -.newsletter-options { - display: grid; - gap: deprecated.$s-16; - margin-inline-start: deprecated.$s-16; -} - -.input-wrapper { - @extend .input-checkbox; -} - -.modal-link { - @include deprecated.bodyLargeTypography; - color: var(--modal-link-foreground-color); - margin: 0; -} - -.accept-btn { - @extend .modal-accept-btn; - justify-self: flex-end; -} diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index a9f69a2c8e..7580b98abd 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -150,7 +150,7 @@ [:* [:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")] [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")] - [:& register/register-methods {:on-success-callback success-register :hide-separator true}] + [:> register/register-methods* {:on-success-callback success-register :hide-separator true}] #_[:hr {:class (stl/css :separator)}] [:div {:class (stl/css :separator)}] [:div {:class (stl/css :change-section)} @@ -160,11 +160,11 @@ :on-click set-section} (tr "auth.login-here")]] [:div {:class (stl/css :links)} [:hr {:class (stl/css :separator)}] - [:& register/terms-register]]] + [:> register/terms-register*]]] :register-validate [:div {:class (stl/css :form-container)} - [:& register/register-form + [:> register/register-form* {:params {:token @register-token} :on-success-callback register-email-sent}] [:div {:class (stl/css :links)} @@ -175,7 +175,7 @@ :register-email-sent [:div {:class (stl/css :form-container)} - [:& register/register-success-page {:params {:email @user-email :hide-logo true}}]] + [:> register/register-success-page* {:params {:email @user-email :hide-logo true}}]] :recovery-request [:& recovery-request-page {:go-back-callback set-section-login diff --git a/frontend/src/app/main/ui/viewer/login.cljs b/frontend/src/app/main/ui/viewer/login.cljs index 30c002a3be..246dd5e44e 100644 --- a/frontend/src/app/main/ui/viewer/login.cljs +++ b/frontend/src/app/main/ui/viewer/login.cljs @@ -12,7 +12,7 @@ [app.main.store :as st] [app.main.ui.auth.login :refer [login-dialog*]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-methods register-success-page terms-register register-validate-form]] + [app.main.ui.auth.register :refer [register-methods* register-success-page* terms-register* register-validate-form*]] [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -94,7 +94,7 @@ :register [:div {:class (stl/css :form-container)} - [:& register-methods {:on-success-callback success-register}] + [:> register-methods* {:on-success-callback success-register}] [:div {:class (stl/css :links)} [:div {:class (stl/css :account)} [:span (tr "auth.already-have-account") " "] @@ -104,8 +104,8 @@ :register-validate [:div {:class (stl/css :form-container)} - [:& register-validate-form {:params {:token @register-token} - :on-success-callback success-email-sent}] + [:> register-validate-form* {:params {:token @register-token} + :on-success-callback success-email-sent}] [:div {:class (stl/css :links)} [:div {:class (stl/css :register)} [:a {:on-click set-section @@ -117,8 +117,8 @@ :on-success-callback success-email-sent}] :email-sent [:div {:class (stl/css :form-container)} - [:& register-success-page {:params {:email @user-email}}]]) + [:> register-success-page* {:params {:email @user-email}}]]) (when main-section [:div {:class (stl/css :links)} - [:& terms-register]])]]])) + [:> terms-register*]])]]])) From cb33fe417e4fdf16e58b1d0233d29f2e57cbea68 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Apr 2026 17:05:55 +0200 Subject: [PATCH 2/8] :bug: Fix non-integer row/column values in grid cell position inputs (#8869) * :bug: Fix non-integer row/column values in grid cell position inputs The numeric-input component allows Alt+arrow key increments of 0.1x the step value, which could produce float values (e.g. 4.5, 0.5) when users adjusted grid cell row/column/row-span/column-span positions. The schema requires these fields to be integers, causing backend validation errors. Round the input values to integers in the on-grid-coordinates callback before passing them to update-grid-cell-position. Signed-off-by: Andrey Antukh * :bug: Enforce integer-only values in grid cell numeric inputs Add an `integer` prop to the legacy `numeric-input*` component that rounds parsed values in `parse-value`, ensuring all input paths (typed text, arrow keys, Alt+arrow, mouse wheel, expressions) produce integers. Use it for all six row/column inputs in the grid cell options panel. Signed-off-by: Andrey Antukh --------- Signed-off-by: Andrey Antukh --- .../src/app/main/ui/components/numeric_input.cljs | 9 +++++++-- .../workspace/sidebar/options/menus/grid_cell.cljs | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 510738b0bd..3d7a2b3e46 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.components.numeric-input (:require [app.common.data :as d] + [app.common.math :as mth] [app.common.schema :as sm] [app.main.ui.formats :as fmt] [app.main.ui.hooks :as h] @@ -43,6 +44,7 @@ step-value (d/parse-double step-value 1) default (d/parse-double default (when-not nillable? 0)) + integer? (unchecked-get props "integer") select-on-focus? (d/nilv (unchecked-get props "selectOnFocus") true) ;; We need a ref pointing to the input dom element, but the user @@ -63,7 +65,7 @@ parse-value (mf/use-fn - (mf/deps min-value max-value value nillable? default) + (mf/deps min-value max-value value nillable? default integer?) (fn [] (when-let [node (mf/ref-val ref)] (let [new-value (-> (dom/get-value node) @@ -72,6 +74,7 @@ (cond (d/num? new-value) (-> new-value + (cond-> integer? mth/round) (d/max (/ sm/min-safe-int 2)) (d/min (/ sm/max-safe-int 2)) (cond-> (d/num? min-value) @@ -146,7 +149,8 @@ (and (d/num? max-value) (> new-value max-value)) max-value - :else new-value)] + :else new-value) + new-value (if integer? (mth/round new-value) new-value)] (apply-value event new-value)))))) @@ -227,6 +231,7 @@ props (-> (obj/clone props) (obj/unset! "selectOnFocus") (obj/unset! "nillable") + (obj/unset! "integer") (obj/set! "value" mf/undefined) (obj/set! "onChange" handle-change) (obj/set! "className" class) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs index 8308bae61e..b2dbd7cf30 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs @@ -10,6 +10,7 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.math :as mth] [app.common.types.shape.layout :as ctl] [app.main.data.workspace :as dw] [app.main.data.workspace.grid-layout.editor :as dwge] @@ -133,7 +134,10 @@ (mf/deps column row (:id shape) (:id cell)) (fn [field type value] (when-not multiple? - (let [[property value] + (let [value (mth/round value) + column (mth/round column) + row (mth/round row) + [property value] (cond (and (= type :column) (or (= field :all) (= field :start))) [:column value] @@ -216,6 +220,7 @@ :title "Column" :on-click #(dom/select-target %) :on-change (partial on-grid-coordinates :all :column) + :integer true :value column}]]] [:div {:class (stl/css :grid-coord-group)} @@ -226,6 +231,7 @@ :title "Row" :on-click #(dom/select-target %) :on-change (partial on-grid-coordinates :all :row) + :integer true :value row}]]]]) (when (and (not multiple?) (or (= :manual cell-mode) (= :area cell-mode))) @@ -237,12 +243,14 @@ {:placeholder "--" :on-pointer-down #(dom/select-target %) :on-change (partial on-grid-coordinates :start :column) + :integer true :value column}]] [:div {:class (stl/css :coord-input)} [:> numeric-input* {:placeholder "--" :on-pointer-down #(dom/select-target %) :on-change (partial on-grid-coordinates :end :column) + :integer true :value column-end}]]] [:div {:class (stl/css :grid-coord-group)} @@ -252,12 +260,14 @@ {:placeholder "--" :on-pointer-down #(dom/select-target %) :on-change (partial on-grid-coordinates :start :row) + :integer true :value row}]] [:div {:class (stl/css :coord-input)} [:> numeric-input* {:placeholder "--" :on-pointer-down #(dom/select-target %) :on-change (partial on-grid-coordinates :end :row) + :integer true :value row-end}]]]]) [:div {:class (stl/css :row)} From 6a0d13171575847aa445944da1ad698eb5e85eed Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Apr 2026 12:06:45 +0000 Subject: [PATCH 3/8] :bug: Fix swapped move-to/line-to type codes in PathData binary readers The impl-walk, impl-reduce, and impl-lookup functions had the binary type codes for move-to and line-to swapped (1 mapped to :line-to and 2 to :move-to). This is inconsistent with from-plain, read-segment, to-string-segment*, and the Rust RawSegmentData enum which all use 1=move-to and 2=line-to. The swap caused incorrect command types to be reported to callers like get-handlers and get-points. Signed-off-by: Andrey Antukh --- common/src/app/common/types/path/impl.cljc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index 959a11bf6f..ecdd7ddb09 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -128,8 +128,8 @@ x (buf/read-float buffer (+ offset 20)) y (buf/read-float buffer (+ offset 24)) type (case type - 1 :line-to - 2 :move-to + 1 :move-to + 2 :line-to 3 :curve-to 4 :close-path) res (f type c1x c1y c2x c2y x y)] @@ -153,8 +153,8 @@ x (buf/read-float buffer (+ offset 20)) y (buf/read-float buffer (+ offset 24)) type (case type - 1 :line-to - 2 :move-to + 1 :move-to + 2 :line-to 3 :curve-to 4 :close-path) result (f result index type c1x c1y c2x c2y x y)] @@ -174,8 +174,8 @@ x (buf/read-float buffer (+ offset 20)) y (buf/read-float buffer (+ offset 24)) type (case type - 1 :line-to - 2 :move-to + 1 :move-to + 2 :line-to 3 :curve-to 4 :close-path)] #?(:clj (f type c1x c1y c2x c2y x y) From b63e4a297b666a913239c33d691a6877bc2826d3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Apr 2026 12:07:07 +0000 Subject: [PATCH 4/8] :bug: Handle corrupted PathData segments gracefully instead of crashing Add nil defaults to all case expressions that match binary segment type codes so that corrupted/unknown values are skipped instead of throwing 'No matching clause'. This prevents a React render crash (triggered via shape-with-open-path? -> get-subpaths -> reduce) when a PathData buffer contains invalid bytes, e.g. from a WASM data transfer or deserialization of damaged stored data. Affected functions: read-segment, impl-walk, impl-reduce, impl-lookup, to-string-segment*, and the seq/reduce protocol implementations on both JVM and CLJS PathData types. Signed-off-by: Andrey Antukh --- common/src/app/common/types/path/impl.cljc | 61 ++++++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index ecdd7ddb09..f15840719f 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -131,8 +131,10 @@ 1 :move-to 2 :line-to 3 :curve-to - 4 :close-path) - res (f type c1x c1y c2x c2y x y)] + 4 :close-path + nil) + res (when (some? type) + (f type c1x c1y c2x c2y x y))] (recur (inc index) (if (some? res) (conj! result res) @@ -156,8 +158,11 @@ 1 :move-to 2 :line-to 3 :curve-to - 4 :close-path) - result (f result index type c1x c1y c2x c2y x y)] + 4 :close-path + nil) + result (if (some? type) + (f result index type c1x c1y c2x c2y x y) + result)] (if (reduced? result) result (recur (inc index) result))) @@ -177,9 +182,11 @@ 1 :move-to 2 :line-to 3 :curve-to - 4 :close-path)] - #?(:clj (f type c1x c1y c2x c2y x y) - :cljs (^function f type c1x c1y c2x c2y x y)))) + 4 :close-path + nil)] + (when (some? type) + #?(:clj (f type c1x c1y c2x c2y x y) + :cljs (^function f type c1x c1y c2x c2y x y))))) (defn- to-string-segment* [buffer offset type ^StringBuilder builder] @@ -219,7 +226,10 @@ (.append ",") (.append y))) 4 (doto builder - (.append "Z")))) + (.append "Z")) + + ;; Skip corrupted/unknown segment types + nil)) (defn- to-string "Format the path data structure to string" @@ -236,7 +246,8 @@ (.toString builder))) (defn- read-segment - "Read segment from binary buffer at specified index" + "Read segment from binary buffer at specified index. Returns nil for + corrupted/invalid segment types." [buffer index] (let [offset (* index SEGMENT-U8-SIZE) type (buf/read-short buffer offset)] @@ -268,7 +279,10 @@ :c2y (double c2y)}}) 4 {:command :close-path - :params {}}))) + :params {}} + + ;; Return nil for corrupted/unknown segment types + nil))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TYPE: PATH-DATA @@ -320,8 +334,10 @@ (when (pos? size) ((fn next-seq [i] (when (< i size) - (cons (read-segment buffer i) - (lazy-seq (next-seq (inc i)))))) + (let [segment (read-segment buffer i)] + (if (some? segment) + (cons segment (lazy-seq (next-seq (inc i)))) + (next-seq (inc i)))))) 0))) clojure.lang.IReduceInit @@ -329,7 +345,10 @@ (loop [index 0 result start] (if (< index size) - (let [result (f result (read-segment buffer index))] + (let [segment (read-segment buffer index) + result (if (some? segment) + (f result segment) + result)] (if (reduced? result) @result (recur (inc index) result))) @@ -407,7 +426,10 @@ (read-segment buffer 0) nil)] (if (< index size) - (let [result (f result (read-segment buffer index))] + (let [segment (read-segment buffer index) + result (if (some? segment) + (f result segment) + result)] (if (reduced? result) @result (recur (inc index) result))) @@ -417,7 +439,10 @@ (loop [index 0 result start] (if (< index size) - (let [result (f result (read-segment buffer index))] + (let [segment (read-segment buffer index) + result (if (some? segment) + (f result segment) + result)] (if (reduced? result) @result (recur (inc index) result))) @@ -446,8 +471,10 @@ (when (pos? size) ((fn next-seq [i] (when (< i size) - (cons (read-segment buffer i) - (lazy-seq (next-seq (inc i)))))) + (let [segment (read-segment buffer i)] + (if (some? segment) + (cons segment (lazy-seq (next-seq (inc i)))) + (next-seq (inc i)))))) 0))) cljs.core/IPrintWithWriter From f97df3e8ab0b1b9ed336b684a502891b5b5ea1e9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Apr 2026 12:38:28 +0000 Subject: [PATCH 5/8] :bug: Fix PathData corruption root causes across WASM and CLJS Replace unsafe std::mem::transmute calls in Rust WASM path code with validated TryFrom conversions to prevent undefined behavior from invalid enum discriminant values. This was the most likely root cause of the "No matching clause: -19772" production crash -- corrupted bytes flowing through transmute could produce arbitrary invalid enum variants. Fix byteOffset handling throughout the CLJS PathData serialization pipeline. DataView instances created via buf/slice carry a non-zero byteOffset, but from-bytes, transit write handler, -write-to, buf/clone, and buf/equals? all operated on the full underlying ArrayBuffer, ignoring offset and length. This could silently produce PathData with incorrect size or content. Rust changes (render-wasm): - RawSegmentData: From<[u8; N]> -> TryFrom<[u8; N]> with discriminant validation (must be 0x01-0x04) before transmuting - RawBoolType: From -> TryFrom with explicit match on 0-3 - Add #[wasm_error] to set_shape_path_content, current_to_path, convert_stroke_to_path, and set_shape_bool_type so panics are caught and routed through the WASM error protocol instead of crashing - set_shape_path_content: replace .expect() with proper Result/? error propagation per segment - Remove unused From bound from SerializableResult trait CLJS changes (common): - from-bytes: use DataView.byteLength instead of ArrayBuffer.byteLength for DataView inputs; preserve byteOffset/byteLength when converting from Uint8Array, Uint32Array, and Int8Array - Transit write handler: construct Uint8Array with byteOffset and byteLength from the DataView, not the full backing ArrayBuffer - -write-to: same byteOffset/byteLength fix - buf/clone: copy only the DataView byte range using Uint8Array with proper offset, not Uint32Array over the full ArrayBuffer - buf/equals?: compare DataView byte ranges using Uint8Array with proper offset, not the full backing ArrayBuffers Frontend changes: - shape-to-path, stroke-to-path, calculate-bool*: wrap WASM call and buffer read in try/catch to ensure mem/free is always called, even when an exception occurs between the WASM call and the free call Signed-off-by: Andrey Antukh --- common/src/app/common/buffer.cljc | 25 +-- common/src/app/common/types/path/impl.cljc | 30 ++-- .../common_tests/types/path_data_test.cljc | 146 ++++++++++++++++++ frontend/src/app/render_wasm/api.cljs | 48 +++--- 4 files changed, 209 insertions(+), 40 deletions(-) diff --git a/common/src/app/common/buffer.cljc b/common/src/app/common/buffer.cljc index 07cf5e6853..7d23edeafe 100644 --- a/common/src/app/common/buffer.cljc +++ b/common/src/app/common/buffer.cljc @@ -217,10 +217,12 @@ (let [buffer (ByteBuffer/wrap dst)] (.order buffer ByteOrder/LITTLE_ENDIAN))) :cljs - (let [buffer' (.-buffer ^js/DataView buffer) - src-view (js/Uint32Array. buffer') - dst-buff (js/ArrayBuffer. (.-byteLength buffer')) - dst-view (js/Uint32Array. dst-buff)] + (let [src-off (.-byteOffset ^js/DataView buffer) + src-len (.-byteLength ^js/DataView buffer) + src-buf (.-buffer ^js/DataView buffer) + src-view (js/Uint8Array. src-buf src-off src-len) + dst-buff (js/ArrayBuffer. src-len) + dst-view (js/Uint8Array. dst-buff)] (.set dst-view src-view) (js/DataView. dst-buff)))) @@ -239,12 +241,15 @@ ^ByteBuffer buffer-b) :cljs - (let [buffer-a (.-buffer buffer-a) - buffer-b (.-buffer buffer-b)] - (if (= (.-byteLength buffer-a) - (.-byteLength buffer-b)) - (let [cb (js/Uint32Array. buffer-a) - ob (js/Uint32Array. buffer-b) + (let [len-a (.-byteLength ^js/DataView buffer-a) + len-b (.-byteLength ^js/DataView buffer-b)] + (if (= len-a len-b) + (let [cb (js/Uint8Array. (.-buffer ^js/DataView buffer-a) + (.-byteOffset ^js/DataView buffer-a) + len-a) + ob (js/Uint8Array. (.-buffer ^js/DataView buffer-b) + (.-byteOffset ^js/DataView buffer-b) + len-b) sz (alength cb)] (loop [i 0] (if (< i sz) diff --git a/common/src/app/common/types/path/impl.cljc b/common/src/app/common/types/path/impl.cljc index f15840719f..2db3fcb2e9 100644 --- a/common/src/app/common/types/path/impl.cljc +++ b/common/src/app/common/types/path/impl.cljc @@ -390,10 +390,10 @@ ;; NOTE: we still use u8 because until the heap refactor merge ;; we can't guarrantee the alignment of offset on 4 bytes (assert (instance? js/ArrayBuffer into-buffer)) - (let [buffer' (.-buffer ^js/DataView buffer) - size (.-byteLength buffer') + (let [size (.-byteLength ^js/DataView buffer) + src-off (.-byteOffset ^js/DataView buffer) mem (js/Uint8Array. into-buffer offset size)] - (.set mem (js/Uint8Array. buffer')))) + (.set mem (js/Uint8Array. (.-buffer ^js/DataView buffer) src-off size)))) ITransformable (-transform [this m] @@ -635,19 +635,27 @@ nil)) (instance? js/DataView buffer) - (let [buffer' (.-buffer ^js/DataView buffer) - size (.-byteLength ^js/ArrayBuffer buffer') - count (long (/ size SEGMENT-U8-SIZE))] + (let [size (.-byteLength ^js/DataView buffer) + count (long (/ size SEGMENT-U8-SIZE))] (PathData. count buffer (weak/weak-value-map) nil)) (instance? js/Uint8Array buffer) - (from-bytes (.-buffer buffer)) + (let [ab (.-buffer buffer) + offset (.-byteOffset buffer) + size (.-byteLength buffer)] + (from-bytes (js/DataView. ab offset size))) (instance? js/Uint32Array buffer) - (from-bytes (.-buffer buffer)) + (let [ab (.-buffer buffer) + offset (.-byteOffset buffer) + size (.-byteLength buffer)] + (from-bytes (js/DataView. ab offset size))) (instance? js/Int8Array buffer) - (from-bytes (.-buffer buffer)) + (let [ab (.-buffer buffer) + offset (.-byteOffset buffer) + size (.-byteLength buffer)] + (from-bytes (js/DataView. ab offset size))) :else (throw (js/Error. "invalid data provided"))))) @@ -733,7 +741,9 @@ :class PathData :wfn (fn [^PathData pdata] (let [buffer (.-buffer pdata)] - #?(:cljs (js/Uint8Array. (.-buffer ^js/DataView buffer)) + #?(:cljs (js/Uint8Array. (.-buffer ^js/DataView buffer) + (.-byteOffset ^js/DataView buffer) + (.-byteLength ^js/DataView buffer)) :clj (.array ^ByteBuffer buffer)))) :rfn from-bytes}) diff --git a/common/test/common_tests/types/path_data_test.cljc b/common/test/common_tests/types/path_data_test.cljc index add7d873ad..252334b459 100644 --- a/common/test/common_tests/types/path_data_test.cljc +++ b/common/test/common_tests/types/path_data_test.cljc @@ -1272,3 +1272,149 @@ (let [segs (vec (:content result)) curve-segs (filter #(= :curve-to (:command %)) segs)] (t/is (pos? (count curve-segs)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TYPE CODE CONSISTENCY TESTS (regression for move-to/line-to swap bug) +;; +;; These tests ensure that all binary reader code paths agree on the +;; mapping: 1=move-to, 2=line-to, 3=curve-to, 4=close-path. +;; +;; The bug was that `impl-walk`, `impl-reduce`, and `impl-lookup` had +;; type codes 1 and 2 swapped (1→:line-to, 2→:move-to) while +;; `read-segment`, `from-plain`, and `to-string-segment*` had the +;; correct mapping. This caused subtle mismatches in operations like +;; `get-subpaths`, `get-points`, `get-handlers`, etc. +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest type-code-walk-consistency + (t/testing "impl-walk produces same command types as read-segment (via vec)" + (let [pdata (path/content sample-content) + ;; read-segment path: produces {:command :keyword ...} maps + seq-types (mapv :command (vec pdata)) + ;; impl-walk path: collects type keywords + walk-types (path.impl/-walk pdata + (fn [type _ _ _ _ _ _] type) + [])] + ;; Both paths must agree on the command types + (t/is (= seq-types walk-types)) + ;; Verify the actual expected types + (t/is (= [:move-to :line-to :curve-to :close-path] seq-types)) + (t/is (= [:move-to :line-to :curve-to :close-path] walk-types))))) + +(t/deftest type-code-reduce-consistency + (t/testing "impl-reduce produces same command types as read-segment (via vec)" + (let [pdata (path/content sample-content) + ;; read-segment path + seq-types (mapv :command (vec pdata)) + ;; impl-reduce path: collects [index type] pairs + reduce-types (path.impl/-reduce + pdata + (fn [acc index type _ _ _ _ _ _] + (conj acc type)) + [])] + (t/is (= seq-types reduce-types)) + (t/is (= [:move-to :line-to :curve-to :close-path] reduce-types))))) + +(t/deftest type-code-lookup-consistency + (t/testing "impl-lookup produces same command types as read-segment for each index" + (let [pdata (path/content sample-content) + seg-count (count pdata)] + (doseq [i (range seg-count)] + (let [;; read-segment path + seg-type (:command (nth pdata i)) + ;; impl-lookup path + lookup-type (path.impl/-lookup + pdata i + (fn [type _ _ _ _ _ _] type))] + (t/is (= seg-type lookup-type) + (str "Mismatch at index " i + ": read-segment=" seg-type + " lookup=" lookup-type))))))) + +(t/deftest type-code-get-points-uses-walk + (t/testing "get-points (via impl-walk) excludes close-path and includes move-to/line-to/curve-to" + (let [pdata (path/content sample-content) + points (path.segment/get-points pdata) + ;; Manually extract points from read-segment (via vec), + ;; skipping close-path + expected-points (->> (vec pdata) + (remove #(= :close-path (:command %))) + (mapv #(gpt/point + (get-in % [:params :x]) + (get-in % [:params :y]))))] + (t/is (= expected-points points)) + ;; Specifically: 3 points (move-to, line-to, curve-to) + (t/is (= 3 (count points)))))) + +(t/deftest type-code-get-subpaths-uses-reduce + (t/testing "get-subpaths (via reduce) correctly identifies move-to to start subpaths" + (let [;; Content with two subpaths: move-to + line-to + close-path, then move-to + line-to + two-subpath-content + [{:command :move-to :params {:x 0.0 :y 0.0}} + {:command :line-to :params {:x 10.0 :y 0.0}} + {:command :close-path :params {}} + {:command :move-to :params {:x 20.0 :y 20.0}} + {:command :line-to :params {:x 30.0 :y 30.0}}] + pdata (path/content two-subpath-content) + subpaths (path.subpath/get-subpaths pdata)] + ;; Must produce exactly 2 subpaths (one per move-to) + (t/is (= 2 (count subpaths))) + ;; First subpath starts at (0,0) + (t/is (= (gpt/point 0.0 0.0) (:from (first subpaths)))) + ;; Second subpath starts at (20,20) + (t/is (= (gpt/point 20.0 20.0) (:from (second subpaths))))))) + +(t/deftest type-code-get-handlers-uses-reduce + (t/testing "get-handlers (via impl-reduce) correctly identifies curve-to segments" + (let [pdata (path/content sample-content) + handlers (path.segment/get-handlers pdata)] + ;; sample-content has one curve-to at index 2 + ;; The curve-to's :c1 handler belongs to the previous point (line-to endpoint) + ;; The curve-to's :c2 handler belongs to the curve-to endpoint + (t/is (some? handlers)) + (let [line-to-point (gpt/point 439.0 802.0) + curve-to-point (gpt/point 264.0 634.0)] + ;; line-to endpoint should have [2 :c1] handler + (t/is (some #(= [2 :c1] %) (get handlers line-to-point))) + ;; curve-to endpoint should have [2 :c2] handler + (t/is (some #(= [2 :c2] %) (get handlers curve-to-point))))))) + +(t/deftest type-code-handler-point-uses-lookup + (t/testing "get-handler-point (via impl-lookup) returns correct values" + (let [pdata (path/content sample-content)] + ;; Index 0 is move-to (480, 839) — not a curve, so any prefix + ;; returns the segment point itself + (let [pt (path.segment/get-handler-point pdata 0 :c1)] + (t/is (= (gpt/point 480.0 839.0) pt))) + ;; Index 2 is curve-to with c1=(368,737), c2=(310,681), point=(264,634) + (let [c1-pt (path.segment/get-handler-point pdata 2 :c1) + c2-pt (path.segment/get-handler-point pdata 2 :c2)] + (t/is (= (gpt/point 368.0 737.0) c1-pt)) + (t/is (= (gpt/point 310.0 681.0) c2-pt)))))) + +(t/deftest type-code-all-readers-agree-large-content + (t/testing "all binary readers agree on types for a large multi-segment path" + (let [pdata (path/content sample-content-large) + seg-count (count pdata) + ;; Collect types from all four code paths + seq-types (mapv :command (vec pdata)) + walk-types (path.impl/-walk pdata + (fn [type _ _ _ _ _ _] type) + []) + reduce-types (path.impl/-reduce + pdata + (fn [acc _ type _ _ _ _ _ _] + (conj acc type)) + []) + lookup-types (mapv (fn [i] + (path.impl/-lookup + pdata i + (fn [type _ _ _ _ _ _] type))) + (range seg-count))] + ;; All four must be identical + (t/is (= seq-types walk-types)) + (t/is (= seq-types reduce-types)) + (t/is (= seq-types lookup-types)) + ;; Verify first and last entries specifically + (t/is (= :move-to (first seq-types))) + (t/is (= :close-path (last seq-types)))))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 849be1089b..a6b402f8fd 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1523,16 +1523,20 @@ (defn shape-to-path [id] (use-shape id) - (let [offset (-> (h/call wasm/internal-module "_current_to_path") - (mem/->offset-32)) - heap (mem/get-heap-u32) - length (aget heap offset) - data (mem/slice heap - (+ offset 1) - (* length path.impl/SEGMENT-U32-SIZE)) - content (path/from-bytes data)] - (mem/free) - content)) + (try + (let [offset (-> (h/call wasm/internal-module "_current_to_path") + (mem/->offset-32)) + heap (mem/get-heap-u32) + length (aget heap offset) + data (mem/slice heap + (+ offset 1) + (* length path.impl/SEGMENT-U32-SIZE)) + content (path/from-bytes data)] + (mem/free) + content) + (catch :default cause + (mem/free) + (throw cause)))) (defn calculate-bool* [bool-type ids] @@ -1545,17 +1549,21 @@ offset (rseq ids)) - (let [offset - (-> (h/call wasm/internal-module "_calculate_bool" (sr/translate-bool-type bool-type)) - (mem/->offset-32)) + (try + (let [offset + (-> (h/call wasm/internal-module "_calculate_bool" (sr/translate-bool-type bool-type)) + (mem/->offset-32)) - length (aget heap offset) - data (mem/slice heap - (+ offset 1) - (* length path.impl/SEGMENT-U32-SIZE)) - content (path/from-bytes data)] - (mem/free) - content))) + length (aget heap offset) + data (mem/slice heap + (+ offset 1) + (* length path.impl/SEGMENT-U32-SIZE)) + content (path/from-bytes data)] + (mem/free) + content) + (catch :default cause + (mem/free) + (throw cause))))) (defn calculate-bool [shape objects] From ffac8d286133016b0a0136b8bd85509df82035e9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 8 Apr 2026 17:34:00 +0200 Subject: [PATCH 6/8] :paperclip: Update changelog --- CHANGES.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d774392169..be1795e06f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,27 @@ # CHANGELOG +## 2.14.2 + +### :sparkles: New features & Enhancements + +- Add protection for stale JS asset cache to force reload on version mismatch [Github #8638](https://github.com/penpot/penpot/pull/8638) +- Normalize newsletter opt-in checkbox across different register flows [Github #8839](https://github.com/penpot/penpot/pull/8839) + +### :bug: Bugs fixed + +- Fix PathData corruption root causes across WASM and CLJS (unsafe transmute and byteOffset handling) +- Handle corrupted PathData segments gracefully instead of crashing +- Fix swapped move-to/line-to type codes in PathData binary readers +- Fix non-integer row/column values in grid cell position inputs [Github #8869](https://github.com/penpot/penpot/pull/8869) +- Fix nil path content crash by exposing safe public API [Github #8806](https://github.com/penpot/penpot/pull/8806) +- Fix infinite recursion in get-frame-ids for thumbnail extraction [Github #8807](https://github.com/penpot/penpot/pull/8807) +- Fix stale-asset detector missing protocol-dispatch errors +- Ignore Zone.js toString TypeError in uncaught error handler [Github #8804](https://github.com/penpot/penpot/pull/8804) +- Prevent thumbnail frame recursion overflow [Github #8763](https://github.com/penpot/penpot/pull/8763) +- Fix vector index out of bounds in viewer zoom-to-fit/fill [Github #8834](https://github.com/penpot/penpot/pull/8834) +- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858) +- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803) + ## 2.14.1 ### :sparkles: New features & Enhancements From 6063c1c53292b04c1d413101d43d1ae1defb7e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Wed, 8 Apr 2026 17:48:05 +0200 Subject: [PATCH 7/8] :books:Clarify remote MCP availability in production (#8910) --- docs/mcp/index.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/mcp/index.md b/docs/mcp/index.md index d09b7db8b7..c338faf96d 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -110,6 +110,18 @@ If you just want to try Penpot MCP quickly, follow this path for the **hosted (r ### Remote MCP in 5 steps +
+ +### Important: remote MCP is not in production yet + +Remote MCP is not available yet in Penpot production (`design.penpot.app`). It is planned for an upcoming release (currently targeted around **2.16**). + +Right now, the remote MCP flow is available only in **testing environments** (for example, instances deployed from the `staging` branch: https://github.com/penpot/penpot/tree/staging). + +If you need MCP in production today, use the **Local MCP server** setup instead. See [Local MCP server](#local-mcp-server). + +
+ 1. #### Enable MCP in Penpot Go to **Your account → Integrations → MCP Server (Beta)** and enable the feature. @@ -286,6 +298,16 @@ In Penpot, open a file and connect the plugin from **File → MCP Server → Con Remote MCP is the easiest way to start using AI agents with Penpot. It's hosted for you, so you don't need to install or run anything on your machine. +
+ +### Availability note + +Remote MCP is currently available only in **testing environments**. It is not yet available in Penpot production (`design.penpot.app`) and is planned for an upcoming release (currently targeted around **2.16**). + +If you need MCP in production today, use the **Local MCP server** setup instead. See [Local MCP server](#local-mcp-server). + +
+ ### Install and activate From dfa45ec8d86ea878f056432efecce9026d22c55d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 9 Apr 2026 09:10:44 +0200 Subject: [PATCH 8/8] :arrow_up: Update deps on root package.json --- package.json | 8 +- pnpm-lock.yaml | 400 ++++++++++++++++++++++++------------------------- 2 files changed, 204 insertions(+), 204 deletions(-) diff --git a/package.json b/package.json index a4e585946d..c0351b106d 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "fmt": "./scripts/fmt" }, "devDependencies": { - "@github/copilot": "^1.0.12", - "@types/node": "^20.12.7", - "esbuild": "^0.27.4", - "opencode-ai": "^1.3.17" + "@github/copilot": "^1.0.21", + "@types/node": "^25.5.2", + "esbuild": "^0.28.0", + "opencode-ai": "^1.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 976eca525e..d06094e10f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,480 +9,480 @@ importers: .: devDependencies: '@github/copilot': - specifier: ^1.0.12 - version: 1.0.12 + specifier: ^1.0.21 + version: 1.0.21 '@types/node': - specifier: ^20.12.7 - version: 20.19.37 + specifier: ^25.5.2 + version: 25.5.2 esbuild: - specifier: ^0.27.4 - version: 0.27.4 + specifier: ^0.28.0 + version: 0.28.0 opencode-ai: - specifier: ^1.3.17 - version: 1.3.17 + specifier: ^1.4.0 + version: 1.4.0 packages: - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@github/copilot-darwin-arm64@1.0.12': - resolution: {integrity: sha512-fjbwRIUZAH06Eyg5ZkfZXg8SVXpqI3HaFhtXZ803CZs9mfIgfOSR3URZxUnv7SIv6aI/7f6ws8RxKnPGavJ/tg==} + '@github/copilot-darwin-arm64@1.0.21': + resolution: {integrity: sha512-aB+s9ldTwcyCOYmzjcQ4SknV6g81z92T8aUJEJZBwOXOTBeWKAJtk16ooAKangZgdwuLgO3or1JUjx1FJAm5nQ==} cpu: [arm64] os: [darwin] hasBin: true - '@github/copilot-darwin-x64@1.0.12': - resolution: {integrity: sha512-/tJGJEEm8kpTW/sJRNnvhMSHKIHApNun14biuIkC5CXDqVgFakbKlckn/FlIkT48eEUysc0YbEatrHIDz/8XbA==} + '@github/copilot-darwin-x64@1.0.21': + resolution: {integrity: sha512-aNad81DOGuGShmaiFNIxBUSZLwte0dXmDYkGfAF9WJIgY4qP4A8CPWFoNr8//gY+4CwaIf9V+f/OC6k2BdECbw==} cpu: [x64] os: [darwin] hasBin: true - '@github/copilot-linux-arm64@1.0.12': - resolution: {integrity: sha512-4977LVJi3/9Yc+ivj+VKDVtHg0kT5yqOrN8F35/jgqerx4Mdtk1pOMlWztXxLicBHN4y2V7/EY/wc0WqFW0Zvg==} + '@github/copilot-linux-arm64@1.0.21': + resolution: {integrity: sha512-FL0NsCnHax4czHVv1S8iBqPLGZDhZ28N3+6nT29xWGhmjBWTkIofxLThKUPcyyMsfPTTxIlrdwWa8qQc5z2Q+g==} cpu: [arm64] os: [linux] hasBin: true - '@github/copilot-linux-x64@1.0.12': - resolution: {integrity: sha512-9QevJZD29PVltYDV4xHWbdN6ud/966clERL5Frh2+9D3+spaVDO1hFllzoFiEwD/M4f2GkSh7/fT3hV0LKl9Ag==} + '@github/copilot-linux-x64@1.0.21': + resolution: {integrity: sha512-S7pWVI16hesZtxYbIyfw+MHZpc5ESoGKUVr5Y+lZJNaM2340gJGPQzQwSpvKIRMLHRKI2hXLwciAnYeMFxE/Tg==} cpu: [x64] os: [linux] hasBin: true - '@github/copilot-win32-arm64@1.0.12': - resolution: {integrity: sha512-RLAbAsLniI8vA2utgZdIsvD8slZpz1fb8l6cmIiQvDE/BwQb2zNV9VepZ+CwzYtNx9ifxBtgIwYwUJq5bxeSaQ==} + '@github/copilot-win32-arm64@1.0.21': + resolution: {integrity: sha512-a9qc2Ku+XbyBkXCclbIvBbIVnECACTIWnPctmXWsQeSdeapGxgfHGux7y8hAFV5j6+nhCm6cnyEMS3rkZjAhdA==} cpu: [arm64] os: [win32] hasBin: true - '@github/copilot-win32-x64@1.0.12': - resolution: {integrity: sha512-4SYV09F4Sw20DAib1do26+ALZmCZrghzo+5e6IZbQOsm4B7NhBFaLpKFU+kEijfmWacLlh/at5CpGGGKlwlbcg==} + '@github/copilot-win32-x64@1.0.21': + resolution: {integrity: sha512-9klu+7NQ6tEyb8sibb0rsbimBivDrnNltZho10Bgbf1wh3o+erTjffXDjW9Zkyaw8lZA9Fz8bqhVkKntZq58Lg==} cpu: [x64] os: [win32] hasBin: true - '@github/copilot@1.0.12': - resolution: {integrity: sha512-GpmoJbs1ECyLLKtY4PcFzO8Cz6GgDTOKkrzwNdkirNdfsB+o6x0OOlFyrOdNXNPII7pk9+GcpIjF87sLwWzpPQ==} + '@github/copilot@1.0.21': + resolution: {integrity: sha512-P+nORjNKAtl92jYCG6Qr1Rsw2JoyScgeQSkIR6O2WB37WS5JVdA4ax1WVualMbfuc9V58CPHX6fwyNpkI89FkQ==} hasBin: true - '@types/node@20.19.37': - resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true - opencode-ai@1.3.17: - resolution: {integrity: sha512-2Awq2za4kLPG9wxFHFmqcmoveTSTeyq7Q3GJ8PoQahjWU17yCjuyJUclouFULzaZgqA8atHOyT/3eUHigMc8Cw==} + opencode-ai@1.4.0: + resolution: {integrity: sha512-Cb5Vo5Rl1gvOIXC8gtgupwoa5+rufsp+6u5tIxIYLl5fReX+P2eugLSOkKH2KB5GC6BwxaEvapEZiPvQYsZSXA==} hasBin: true - opencode-darwin-arm64@1.3.17: - resolution: {integrity: sha512-aaMXeNQRPLdGPoxULFty1kxYxT2qPXCiqftYbLF2SQN9Xjq8BR3BjA766ncae1hdiDJJAe1CSpWDbobn5K+oyA==} + opencode-darwin-arm64@1.4.0: + resolution: {integrity: sha512-rXdrH1Oejb+220ZCzkd1P+tCP7IhLTyfRbUr89vzvEWVRueh0vr2hvyrGDVv9LAskZAt/hwY3Wnw9CzjtxocdQ==} cpu: [arm64] os: [darwin] - opencode-darwin-x64-baseline@1.3.17: - resolution: {integrity: sha512-ftEiCwzl6OjIqpXD075lHWHT1YKJjNDPvL1XlLDv86Wt4Noc818fl1lOWwg/LkNL04LoXD2oa3NGOJZYzd6STQ==} + opencode-darwin-x64-baseline@1.4.0: + resolution: {integrity: sha512-5xCXF8Xn9M2WQKZATc4llm9qrAc4JodmQj88Oibbz/rXIOr/A1ejXvaeqLOQkQyQweeEABlYXOf3eCiY5hx8Gw==} cpu: [x64] os: [darwin] - opencode-darwin-x64@1.3.17: - resolution: {integrity: sha512-fMlnOCtaMnwimdP81a3F7QK9GUwhrQnxaKuUZk31wYcGBGQKgSSdy2xK8CRLcaHEV8gLxSlcGJj7g4NTOrC9Tw==} + opencode-darwin-x64@1.4.0: + resolution: {integrity: sha512-PhBfT2EtPos7jcGBtVSz3+yKv2e1nQy1UrXiH4ILdSgwzroKU/0kMsRtWJeMPHIj1imUQmSVlnDcuxiCiCkozw==} cpu: [x64] os: [darwin] - opencode-linux-arm64-musl@1.3.17: - resolution: {integrity: sha512-clD6K35+pP60xLiqCJFTTTpDK2XFahOlSo8TQckXCvCnYYwMqdK9sOO7uzDHLNyPIGLKiYNZTxqVazuGnbGmYQ==} + opencode-linux-arm64-musl@1.4.0: + resolution: {integrity: sha512-1lc0Nj6OmtJF5NJn+AhV7rXOHzw+0p7rvXQu2iqd9V7DpUEbltyF6oiyuF54gBZjHpvSzFXu8a3YeTcuMEXdNA==} cpu: [arm64] os: [linux] - opencode-linux-arm64@1.3.17: - resolution: {integrity: sha512-gd4kndxNwYi9kINyrTItY35M7UZ4jAXMxbbdbFnUBFYI009uv4bgNofnZnVOAFfjc0/PpxSgdNn9eHDjlJEdJQ==} + opencode-linux-arm64@1.4.0: + resolution: {integrity: sha512-XEM3tP7DTrYDEYCe9jqC/xtgzPJpCZTfinc5DjcPuh2hx+iHCGSr9+qG7tRGeCyvD9ibAFewNtpco5Is49JCrg==} cpu: [arm64] os: [linux] - opencode-linux-x64-baseline-musl@1.3.17: - resolution: {integrity: sha512-BiNu5B6QfohG+KwNcY3YlKR465DNke0nknRqn3Ke2APp6GghuimlnyEKcji1brhZsdjdembc79KWfQcsHlYsyA==} + opencode-linux-x64-baseline-musl@1.4.0: + resolution: {integrity: sha512-URg1JXIUaNz0R4TLUT98rK2jozmh5ScAkkqxPK6LWj3XwJojJx23mJRNgLb26034PgNkUwXhrtdbnyPTSVlkqQ==} cpu: [x64] os: [linux] - opencode-linux-x64-baseline@1.3.17: - resolution: {integrity: sha512-OIp+jdr9Rus6kAVWgB8cuGMRPFVJdMwQvjOfprbgoM2KUIjgXKsXgyCmetKZIH/iadmVffjv7p6QrYWFDh6cBA==} + opencode-linux-x64-baseline@1.4.0: + resolution: {integrity: sha512-GocjLGNgs41PLgSVPWxT3Do0StZkDB9QF3e3VIIAGzPmOVcpTZLdDvJPkZdRbRGcVfUcSRGquBbBgvwK9Zsw4w==} cpu: [x64] os: [linux] - opencode-linux-x64-musl@1.3.17: - resolution: {integrity: sha512-/GfRB+vuadE6KAM0kxPQHno3ywxBfiRJp5uZLLKSGAEunXo9az1wkmSR97g4tnxHD4F59hjYOloK9XQQIDwgog==} + opencode-linux-x64-musl@1.4.0: + resolution: {integrity: sha512-yGb1uNO++BtkZ7X/LGLax9ppaEvsmn5s5GaAqcrYj/SyJA5cL2IYzEeMYRAsrb0b81fQCSq5SLEiWiMq1o59oA==} cpu: [x64] os: [linux] - opencode-linux-x64@1.3.17: - resolution: {integrity: sha512-FmoKpX+g78qi4MPvRMWZMZZYKVuH7qkNFXEqGUb0wtixvwuWYvqmUeF9D0GLM/rZnGA33sW6nCkro8aCuyR0Bw==} + opencode-linux-x64@1.4.0: + resolution: {integrity: sha512-Ops08slOBhHbKaYhERH8zMTjlM6mearVaA0udCDIx2fGqDbZRisoRyyI6Z44GPYBH02w8eGmvOvnF5fQYyq2fw==} cpu: [x64] os: [linux] - opencode-windows-arm64@1.3.17: - resolution: {integrity: sha512-gXZ+JKwCUZ9yjVilvnn6zg5vvRy0oPgqIO6qyfvXiLXV+UWJaSTlXl6/4CeXOkvvYeXhLdCtIFii2jbQJjHR3g==} + opencode-windows-arm64@1.4.0: + resolution: {integrity: sha512-47quWER7bCGRPWRXd3fsOyu5F/T4Y65FiS05kD+PYYV4iOJymlBQ34kpcJhNBOpQLYf9HSLbJ8AaJeb5dmUi+Q==} cpu: [arm64] os: [win32] - opencode-windows-x64-baseline@1.3.17: - resolution: {integrity: sha512-Q61MuJBTt+qLyClTEaqbCHh3Fivx0eZ1vHKlhEk7MfIdP/LoDbvSitNRUgtsU/C+ct5Y+c6JXOlxlaFFpqybeA==} + opencode-windows-x64-baseline@1.4.0: + resolution: {integrity: sha512-eGK9lF70XKzf9zBO7xil9+Vl7ZJUAgLK6bG6kug6RKxD6FsydY3Y6q/3tIW0+YZ0wyINOtEbTRfUHbO5TxV4FQ==} cpu: [x64] os: [win32] - opencode-windows-x64@1.3.17: - resolution: {integrity: sha512-+arPhczUa5NBH/thsKAxLmXgkB2WAxtj8Dd293GJZBBEXRhWF1jsXbLvGLY3qDBbvXm9XR7CkJqL1at344pQLw==} + opencode-windows-x64@1.4.0: + resolution: {integrity: sha512-DQ8CoxCsmFM38U1e73+hFuB6Wu0tbn6B4R7KwcL1JhvKvQaYYiukNfuLgcjjx5D7s81NP1SWlv6lw60wN0gq8g==} cpu: [x64] os: [win32] - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} snapshots: - '@esbuild/aix-ppc64@0.27.4': + '@esbuild/aix-ppc64@0.28.0': optional: true - '@esbuild/android-arm64@0.27.4': + '@esbuild/android-arm64@0.28.0': optional: true - '@esbuild/android-arm@0.27.4': + '@esbuild/android-arm@0.28.0': optional: true - '@esbuild/android-x64@0.27.4': + '@esbuild/android-x64@0.28.0': optional: true - '@esbuild/darwin-arm64@0.27.4': + '@esbuild/darwin-arm64@0.28.0': optional: true - '@esbuild/darwin-x64@0.27.4': + '@esbuild/darwin-x64@0.28.0': optional: true - '@esbuild/freebsd-arm64@0.27.4': + '@esbuild/freebsd-arm64@0.28.0': optional: true - '@esbuild/freebsd-x64@0.27.4': + '@esbuild/freebsd-x64@0.28.0': optional: true - '@esbuild/linux-arm64@0.27.4': + '@esbuild/linux-arm64@0.28.0': optional: true - '@esbuild/linux-arm@0.27.4': + '@esbuild/linux-arm@0.28.0': optional: true - '@esbuild/linux-ia32@0.27.4': + '@esbuild/linux-ia32@0.28.0': optional: true - '@esbuild/linux-loong64@0.27.4': + '@esbuild/linux-loong64@0.28.0': optional: true - '@esbuild/linux-mips64el@0.27.4': + '@esbuild/linux-mips64el@0.28.0': optional: true - '@esbuild/linux-ppc64@0.27.4': + '@esbuild/linux-ppc64@0.28.0': optional: true - '@esbuild/linux-riscv64@0.27.4': + '@esbuild/linux-riscv64@0.28.0': optional: true - '@esbuild/linux-s390x@0.27.4': + '@esbuild/linux-s390x@0.28.0': optional: true - '@esbuild/linux-x64@0.27.4': + '@esbuild/linux-x64@0.28.0': optional: true - '@esbuild/netbsd-arm64@0.27.4': + '@esbuild/netbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-x64@0.27.4': + '@esbuild/netbsd-x64@0.28.0': optional: true - '@esbuild/openbsd-arm64@0.27.4': + '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/openbsd-x64@0.27.4': + '@esbuild/openbsd-x64@0.28.0': optional: true - '@esbuild/openharmony-arm64@0.27.4': + '@esbuild/openharmony-arm64@0.28.0': optional: true - '@esbuild/sunos-x64@0.27.4': + '@esbuild/sunos-x64@0.28.0': optional: true - '@esbuild/win32-arm64@0.27.4': + '@esbuild/win32-arm64@0.28.0': optional: true - '@esbuild/win32-ia32@0.27.4': + '@esbuild/win32-ia32@0.28.0': optional: true - '@esbuild/win32-x64@0.27.4': + '@esbuild/win32-x64@0.28.0': optional: true - '@github/copilot-darwin-arm64@1.0.12': + '@github/copilot-darwin-arm64@1.0.21': optional: true - '@github/copilot-darwin-x64@1.0.12': + '@github/copilot-darwin-x64@1.0.21': optional: true - '@github/copilot-linux-arm64@1.0.12': + '@github/copilot-linux-arm64@1.0.21': optional: true - '@github/copilot-linux-x64@1.0.12': + '@github/copilot-linux-x64@1.0.21': optional: true - '@github/copilot-win32-arm64@1.0.12': + '@github/copilot-win32-arm64@1.0.21': optional: true - '@github/copilot-win32-x64@1.0.12': + '@github/copilot-win32-x64@1.0.21': optional: true - '@github/copilot@1.0.12': + '@github/copilot@1.0.21': optionalDependencies: - '@github/copilot-darwin-arm64': 1.0.12 - '@github/copilot-darwin-x64': 1.0.12 - '@github/copilot-linux-arm64': 1.0.12 - '@github/copilot-linux-x64': 1.0.12 - '@github/copilot-win32-arm64': 1.0.12 - '@github/copilot-win32-x64': 1.0.12 + '@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 - '@types/node@20.19.37': + '@types/node@25.5.2': dependencies: - undici-types: 6.21.0 + undici-types: 7.18.2 - esbuild@0.27.4: + esbuild@0.28.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 - opencode-ai@1.3.17: + opencode-ai@1.4.0: optionalDependencies: - opencode-darwin-arm64: 1.3.17 - opencode-darwin-x64: 1.3.17 - opencode-darwin-x64-baseline: 1.3.17 - opencode-linux-arm64: 1.3.17 - opencode-linux-arm64-musl: 1.3.17 - opencode-linux-x64: 1.3.17 - opencode-linux-x64-baseline: 1.3.17 - opencode-linux-x64-baseline-musl: 1.3.17 - opencode-linux-x64-musl: 1.3.17 - opencode-windows-arm64: 1.3.17 - opencode-windows-x64: 1.3.17 - opencode-windows-x64-baseline: 1.3.17 + opencode-darwin-arm64: 1.4.0 + opencode-darwin-x64: 1.4.0 + opencode-darwin-x64-baseline: 1.4.0 + opencode-linux-arm64: 1.4.0 + opencode-linux-arm64-musl: 1.4.0 + opencode-linux-x64: 1.4.0 + opencode-linux-x64-baseline: 1.4.0 + opencode-linux-x64-baseline-musl: 1.4.0 + opencode-linux-x64-musl: 1.4.0 + opencode-windows-arm64: 1.4.0 + opencode-windows-x64: 1.4.0 + opencode-windows-x64-baseline: 1.4.0 - opencode-darwin-arm64@1.3.17: + opencode-darwin-arm64@1.4.0: optional: true - opencode-darwin-x64-baseline@1.3.17: + opencode-darwin-x64-baseline@1.4.0: optional: true - opencode-darwin-x64@1.3.17: + opencode-darwin-x64@1.4.0: optional: true - opencode-linux-arm64-musl@1.3.17: + opencode-linux-arm64-musl@1.4.0: optional: true - opencode-linux-arm64@1.3.17: + opencode-linux-arm64@1.4.0: optional: true - opencode-linux-x64-baseline-musl@1.3.17: + opencode-linux-x64-baseline-musl@1.4.0: optional: true - opencode-linux-x64-baseline@1.3.17: + opencode-linux-x64-baseline@1.4.0: optional: true - opencode-linux-x64-musl@1.3.17: + opencode-linux-x64-musl@1.4.0: optional: true - opencode-linux-x64@1.3.17: + opencode-linux-x64@1.4.0: optional: true - opencode-windows-arm64@1.3.17: + opencode-windows-arm64@1.4.0: optional: true - opencode-windows-x64-baseline@1.3.17: + opencode-windows-x64-baseline@1.4.0: optional: true - opencode-windows-x64@1.3.17: + opencode-windows-x64@1.4.0: optional: true - undici-types@6.21.0: {} + undici-types@7.18.2: {}