diff --git a/.gitignore b/.gitignore index b295d6aeac..79e8626277 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ /.idea /.claude /.playwright-mcp +/tools/__pycache__ diff --git a/CHANGES.md b/CHANGES.md index db73c522f2..311a5449ba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -134,7 +134,11 @@ - Fix incorrect error message when applying tokens while editing text [#9620](https://github.com/penpot/penpot/issues/9620) (PR: [#9708](https://github.com/penpot/penpot/pull/9708)) -## 2.15.4 (Unreleased) +## 2.15.4 + +### :sparkles: New features & Enhancements + +- Add rate limiting and concurrency safety for file snapshot operations [#9723](https://github.com/penpot/penpot/issues/9723) (PR: [#9722](https://github.com/penpot/penpot/pull/9722)) ### :bug: Bugs fixed @@ -143,6 +147,7 @@ - Fix API doc endpoint returning HTML as text/plain [#9680](https://github.com/penpot/penpot/issues/9680) (PR: [#9681](https://github.com/penpot/penpot/pull/9681)) - Fix unexpected error when opening the export dialog [#9721](https://github.com/penpot/penpot/issues/9721) (PR: [#9704](https://github.com/penpot/penpot/pull/9704)) + ## 2.15.3 ### :bug: Bugs fixed diff --git a/backend/resources/climit.edn b/backend/resources/climit.edn index 34d2184153..7d8234499b 100644 --- a/backend/resources/climit.edn +++ b/backend/resources/climit.edn @@ -19,7 +19,7 @@ {:permits 40} :root/by-profile - {:permits 10} + {:permits 10 :queue 30 :timeout 30000} :file-thumbnail-ops/global {:permits 20} @@ -27,4 +27,16 @@ {:permits 2} :submit-audit-events/by-profile - {:permits 1 :queue 3}} + {:permits 1 :queue 3} + + :restore-file-snapshot/global + {:permits 3} + + :restore-file-snapshot/by-profile + {:permits 1 :queue 2 :timeout 60000} + + :create-file-snapshot/global + {:permits 3} + + :create-file-snapshot/by-profile + {:permits 1 :queue 2 :timeout 60000}} diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index c23ea07524..de80175040 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -27,7 +27,9 @@ [next.jdbc.transaction]) (:import com.zaxxer.hikari.HikariConfig + com.zaxxer.hikari.HikariConfigMXBean com.zaxxer.hikari.HikariDataSource + com.zaxxer.hikari.HikariPoolMXBean com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory io.whitfin.siphash.SipHasher io.whitfin.siphash.SipHasherContainer @@ -67,9 +69,8 @@ (def defaults {::name :main - ::min-size 0 ::max-size 60 - ::connection-timeout 10000 + ::connection-timeout 30000 ::validation-timeout 10000 ::idle-timeout 120000 ; 2min ::max-lifetime 1800000 ; 30m @@ -82,7 +83,7 @@ (defmethod ig/init-key ::pool [_ cfg] (let [{:keys [::uri ::read-only] :as cfg} - (merge defaults cfg)] + (merge defaults (d/without-nils cfg))] (when uri (l/info :hint "initialize connection pool" :name (d/name (::name cfg)) @@ -90,7 +91,8 @@ :read-only read-only :credentials (and (contains? cfg ::username) (contains? cfg ::password)) - :min-size (::min-size cfg) + :min-size (or (::min-size cfg) + (::max-size cfg)) :max-size (::max-size cfg)) (create-pool cfg)))) @@ -111,7 +113,9 @@ [{:keys [::uri] :as cfg}] ;; (app.common.pprint/pprint cfg) - (let [config (HikariConfig.)] + (let [config (HikariConfig.) + max-size (::max-size cfg) + min-size (or (::min-size cfg) max-size)] (doto config (.setJdbcUrl (str "jdbc:" uri)) (.setPoolName (d/name (::name cfg))) @@ -121,8 +125,8 @@ (.setValidationTimeout (::validation-timeout cfg)) (.setIdleTimeout (::idle-timeout cfg)) (.setMaxLifetime (::max-lifetime cfg)) - (.setMinimumIdle (::min-size cfg)) - (.setMaximumPoolSize (::max-size cfg)) + (.setMinimumIdle min-size) + (.setMaximumPoolSize max-size) (.setConnectionInitSql initsql) (.setInitializationFailTimeout -1)) @@ -180,6 +184,20 @@ :code :invalid-connection :hint "invalid connection provided"))) +(defn pool-stats + "Given a HikariDataSource instance, returns a map with current pool + statistics: active/idle connections, threads awaiting connection, + total connections, maximum pool size, and minimum idle connections." + [^HikariDataSource pool] + (let [^HikariPoolMXBean pool-mxbean (.getHikariPoolMXBean pool) + ^HikariConfigMXBean cfg-mxbean (.getHikariConfigMXBean pool)] + {:active-connections (.getActiveConnections pool-mxbean) + :idle-connections (.getIdleConnections pool-mxbean) + :threads-awaiting-connection (.getThreadsAwaitingConnection pool-mxbean) + :total-connections (.getTotalConnections pool-mxbean) + :maximum-pool-size (.getMaximumPoolSize cfg-mxbean) + :minimum-idle (.getMinimumIdle cfg-mxbean)})) + (defn create-pool [cfg] (let [dsc (create-datasource-config cfg)] diff --git a/backend/src/app/features/file_snapshots.clj b/backend/src/app/features/file_snapshots.clj index e013b90d00..b347367003 100644 --- a/backend/src/app/features/file_snapshots.clj +++ b/backend/src/app/features/file_snapshots.clj @@ -66,11 +66,6 @@ LEFT JOIN file_data AS fd ON (fd.file_id = f.id AND fd.id = f.id) WHERE f.id = ?") -(defn- get-minimal-file - [cfg id & {:as opts}] - (-> (db/get-with-sql cfg [sql:get-minimal-file id] opts) - (d/update-when :metadata fdata/decode-metadata))) - (def ^:private sql:get-snapshot-without-data (str "WITH snapshots AS (" sql:snapshots ")" "SELECT c.id, @@ -112,7 +107,7 @@ THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz) END")) -(defn get-snapshot-data +(defn get-snapshot "Get a fully decoded snapshot for read-only preview or restoration. Returns the snapshot map with decoded :data field." [cfg file-id snapshot-id] @@ -320,79 +315,87 @@ (defn restore! [{:keys [::db/conn] :as cfg} file-id snapshot-id] - (let [file (get-minimal-file conn file-id {::db/for-update true}) - vern (rand-int Integer/MAX_VALUE) + (let [lock-sql (str sql:get-minimal-file " FOR UPDATE OF f SKIP LOCKED") + row (db/exec-one! conn [lock-sql file-id])] - storage - (sto/resolve cfg {::db/reuse-conn true}) + (when-not row + (ex/raise :type :conflict + :code :file-locked + :hint "the file is currently locked by another operation, retry later")) - snapshot - (get-snapshot-data cfg file-id snapshot-id)] + (let [file (d/update-when row :metadata fdata/decode-metadata) + vern (rand-int Integer/MAX_VALUE) - (when-not snapshot - (ex/raise :type :not-found - :code :snapshot-not-found - :hint "unable to find snapshot with the provided label" - :snapshot-id snapshot-id - :file-id file-id)) + storage + (sto/resolve cfg {::db/reuse-conn true}) - (when-not (:data snapshot) - (ex/raise :type :internal - :code :snapshot-without-data - :hint "snapshot has no data" - :label (:label snapshot) - :file-id file-id)) + snapshot + (get-snapshot cfg file-id snapshot-id)] - (let [;; If the snapshot has applied migrations stored, we reuse - ;; them, if not, we take a safest set of migrations as - ;; starting point. This is because, at the time of - ;; implementing snapshots, migrations were not taken into - ;; account so we need to make this backward compatible in - ;; some way. - migrations - (or (:migrations snapshot) - (fmg/generate-migrations-from-version 67)) + (when-not snapshot + (ex/raise :type :not-found + :code :snapshot-not-found + :hint "unable to find snapshot with the provided label" + :snapshot-id snapshot-id + :file-id file-id)) - file - (-> file - (update :revn inc) - (assoc :migrations migrations) - (assoc :data (:data snapshot)) - (assoc :vern vern) - (assoc :version (:version snapshot)) - (assoc :has-media-trimmed false) - (assoc :modified-at (:modified-at snapshot)) - (assoc :features (:features snapshot)))] + (when-not (:data snapshot) + (ex/raise :type :internal + :code :snapshot-without-data + :hint "snapshot has no data" + :label (:label snapshot) + :file-id file-id)) - (l/dbg :hint "restoring snapshot" - :file-id (str file-id) - :label (:label snapshot) - :snapshot-id (str (:id snapshot))) + (let [;; If the snapshot has applied migrations stored, we reuse + ;; them, if not, we take a safest set of migrations as + ;; starting point. This is because, at the time of + ;; implementing snapshots, migrations were not taken into + ;; account so we need to make this backward compatible in + ;; some way. + migrations + (or (:migrations snapshot) + (fmg/generate-migrations-from-version 67)) - ;; In the same way, on reseting the file data, we need to restore - ;; the applied migrations on the moment of taking the snapshot - (bfc/update-file! cfg file ::bfc/reset-migrations? true) + file + (-> file + (update :revn inc) + (assoc :migrations migrations) + (assoc :data (:data snapshot)) + (assoc :vern vern) + (assoc :version (:version snapshot)) + (assoc :has-media-trimmed false) + (assoc :modified-at (:modified-at snapshot)) + (assoc :features (:features snapshot)))] - ;; FIXME: this should be separated functions, we should not have - ;; inline sql here. + (l/dbg :hint "restoring snapshot" + :file-id (str file-id) + :label (:label snapshot) + :snapshot-id (str (:id snapshot))) - ;; clean object thumbnails - (let [sql (str "update file_tagged_object_thumbnail " - " set deleted_at = now() " - " where file_id=? returning media_id") - res (db/exec! conn [sql file-id])] - (doseq [media-id (into #{} (keep :media-id) res)] - (sto/touch-object! storage media-id))) + ;; In the same way, on reseting the file data, we need to restore + ;; the applied migrations on the moment of taking the snapshot + (bfc/update-file! cfg file ::bfc/reset-migrations? true) - ;; clean file thumbnails - (let [sql (str "update file_thumbnail " - " set deleted_at = now() " - " where file_id=? returning media_id") - res (db/exec! conn [sql file-id])] - (doseq [media-id (into #{} (keep :media-id) res)] - (sto/touch-object! storage media-id))) + ;; FIXME: this should be separated functions, we should not have + ;; inline sql here. - vern))) + ;; clean object thumbnails + (let [sql (str "update file_tagged_object_thumbnail " + " set deleted_at = now() " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/touch-object! storage media-id))) + + ;; clean file thumbnails + (let [sql (str "update file_thumbnail " + " set deleted_at = now() " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/touch-object! storage media-id))) + + vern)))) (defn delete! [cfg & {:keys [id file-id deleted-at]}] diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 940775bdf0..2c154ea95c 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -154,8 +154,8 @@ ::db/username (cf/get :database-username) ::db/password (cf/get :database-password) ::db/read-only (cf/get :database-readonly false) - ::db/min-size (cf/get :database-min-pool-size 0) - ::db/max-size (cf/get :database-max-pool-size 60) + ::db/min-size (cf/get :database-min-pool-size) + ::db/max-size (cf/get :database-max-pool-size) ::mtx/metrics (ig/ref ::mtx/metrics)} ;; Default netty IO pool (shared between several services) diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj index 7736b66cd9..7ac6d51cf4 100644 --- a/backend/src/app/rpc/commands/files_snapshot.clj +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -18,6 +18,7 @@ [app.main :as-alias main] [app.msgbus :as mbus] [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] [app.rpc.commands.files :as files] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] @@ -54,7 +55,7 @@ [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}] (let [perms (bfc/get-file-permissions conn profile-id file-id)] (files/check-read-permissions! perms) - (let [snapshot (fsnap/get-snapshot-data cfg file-id id)] + (let [snapshot (fsnap/get-snapshot cfg file-id id)] (when-not snapshot (ex/raise :type :not-found :code :snapshot-not-found @@ -81,9 +82,10 @@ (sv/defmethod ::create-file-snapshot {::doc/added "1.20" ::sm/params schema:create-file-snapshot - ::db/transaction true} - [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}] - (files/check-edition-permissions! conn profile-id file-id) + ::climit/id [[:create-file-snapshot/by-profile ::rpc/profile-id] + [:create-file-snapshot/global]]} + [cfg {:keys [::rpc/profile-id file-id label]}] + (files/check-edition-permissions! cfg profile-id file-id) (let [file (bfc/get-file cfg file-id :realize? true) project (db/get-by-id cfg :project (:project-id file))] @@ -95,10 +97,10 @@ (quotes/check! {::quotes/id ::quotes/snapshots-per-file} {::quotes/id ::quotes/snapshots-per-team})) - (fsnap/create! cfg file - {:label label - :profile-id profile-id - :created-by "user"}))) + (db/tx-run! cfg fsnap/create! file + {:label label + :profile-id profile-id + :created-by "user"}))) (def ^:private schema:restore-file-snapshot [:map {:title "restore-file-snapshot"} @@ -108,29 +110,43 @@ (sv/defmethod ::restore-file-snapshot {::doc/added "1.20" ::sm/params schema:restore-file-snapshot - ::db/transaction true} - [{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}] - (files/check-edition-permissions! conn profile-id file-id) + ::climit/id [[:restore-file-snapshot/by-profile ::rpc/profile-id] + [:restore-file-snapshot/global]]} + [{:keys [::db/pool ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}] + + ;; Check permissions and read current file state (short-lived, outside restore transaction) + (files/check-edition-permissions! pool profile-id file-id) (let [file (bfc/get-file cfg file-id) - team (teams/get-team conn + team (teams/get-team pool :profile-id profile-id :file-id file-id) - delay (ldel/get-deletion-delay team)] + delay (ldel/get-deletion-delay team) + file-revn (:revn file)] + ;; Create backup snapshot of the current state (committed immediately + ;; independently of the restore outcome) (fsnap/create! cfg file {:profile-id profile-id :deleted-at (ct/in-future delay) :created-by "system"}) - (let [vern (fsnap/restore! cfg file-id id)] - ;; Send to the clients a notification to reload the file - (mbus/pub! msgbus - :topic (:id file) - :message {:type :file-restored - :session-id session-id - :file-id (:id file) - :vern vern}) - nil))) + ;; Restore snapshot inside its own transaction; the revn check + ;; ensures no data is lost if the file was edited concurrently + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [current (bfc/get-minimal-file conn file-id {::db/for-update true})] + (when (not= (:revn current) file-revn) + (ex/raise :type :conflict + :code :file-modified + :hint "the file was modified during the restore process, please retry"))) + (let [vern (fsnap/restore! cfg file-id id)] + (mbus/pub! msgbus + :topic (:id file) + :message {:type :file-restored + :session-id session-id + :file-id (:id file) + :vern vern}) + nil))))) (def ^:private schema:update-file-snapshot [:map {:title "update-file-snapshot"} diff --git a/backend/test/backend_tests/db_test.clj b/backend/test/backend_tests/db_test.clj new file mode 100644 index 0000000000..b61ce6c920 --- /dev/null +++ b/backend/test/backend_tests/db_test.clj @@ -0,0 +1,43 @@ +;; 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.db-test + (:require + [app.db :as db] + [backend-tests.helpers :as th] + [clojure.test :as t]) + (:import + com.zaxxer.hikari.HikariConfig + com.zaxxer.hikari.HikariDataSource + java.sql.Connection)) + +(t/use-fixtures :once th/state-init) + +(t/deftest pool-stats-returns-expected-keys + (let [stats (db/pool-stats th/*pool*)] + (t/testing "all expected keys are present" + (t/is (contains? stats :active-connections)) + (t/is (contains? stats :idle-connections)) + (t/is (contains? stats :threads-awaiting-connection)) + (t/is (contains? stats :total-connections)) + (t/is (contains? stats :maximum-pool-size)) + (t/is (contains? stats :minimum-idle))) + + (t/testing "values are non-negative integers" + (t/is (>= (:active-connections stats) 0)) + (t/is (>= (:idle-connections stats) 0)) + (t/is (>= (:threads-awaiting-connection stats) 0)) + (t/is (>= (:total-connections stats) 0)) + (t/is (>= (:maximum-pool-size stats) 0)) + (t/is (>= (:minimum-idle stats) 0))) + + (t/testing "total connections equals active + idle" + (t/is (= (:total-connections stats) + (+ (:active-connections stats) + (:idle-connections stats))))) + + (t/testing "maximum pool size is reasonable" + (t/is (pos? (:maximum-pool-size stats)))))) diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 25d419a354..cd5c9bf113 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -89,6 +89,7 @@ services: depends_on: - penpot-backend - penpot-exporter + - penpot-mcp networks: - penpot @@ -106,7 +107,8 @@ services: environment: << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri] - + # Set to "true" on hosts where IPv6 is disabled at kernel boot level. + # PENPOT_DISABLE_IPV6_LISTEN: "true" penpot-backend: image: "penpotapp/backend:${PENPOT_VERSION:-2.15}" restart: always diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 571c9f6782..0948d38c61 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -46,7 +46,11 @@ export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000} export PENPOT_MCP_URI=${PENPOT_MCP_URI:-http://penpot-mcp:4401} export PENPOT_MCP_URI_WS=${PENPOT_MCP_URI_WS:-http://penpot-mcp:4402} 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_MCP_URI,\$PENPOT_MCP_URI_WS,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \ +export PENPOT_IPV6_LISTEN_DIRECTIVE=${PENPOT_IPV6_LISTEN_DIRECTIVE:-"listen [::]:8080 default_server;"} +if [ "${PENPOT_DISABLE_IPV6_LISTEN}" = "true" ]; then + export PENPOT_IPV6_LISTEN_DIRECTIVE="" +fi +envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_MCP_URI,\$PENPOT_MCP_URI_WS,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE,\$PENPOT_IPV6_LISTEN_DIRECTIVE" \ < /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 c182856e6b..6efcbc47f3 100644 --- a/docker/images/files/nginx.conf.template +++ b/docker/images/files/nginx.conf.template @@ -73,7 +73,7 @@ http { server { listen 8080 default_server; - listen [::]:8080 default_server; + ${PENPOT_IPV6_LISTEN_DIRECTIVE} server_name _; client_max_body_size $PENPOT_HTTP_SERVER_MAX_BODY_SIZE; diff --git a/frontend/src/app/main/ui/releases/v2_15.cljs b/frontend/src/app/main/ui/releases/v2_15.cljs index 76f6527f02..1064689d17 100644 --- a/frontend/src/app/main/ui/releases/v2_15.cljs +++ b/frontend/src/app/main/ui/releases/v2_15.cljs @@ -77,7 +77,7 @@ [:> c/navigation-bullets* {:slide slide :navigate navigate - :total 4}] + :total 3}] [:button {:on-click next :class (stl/css :next-btn)} "Continue"]]]]]] @@ -118,7 +118,7 @@ [:> c/navigation-bullets* {:slide slide :navigate navigate - :total 4}] + :total 3}] [:button {:on-click next :class (stl/css :next-btn)} "Continue"]]]]]] diff --git a/package.json b/package.json index 543592511c..d4a799d6f3 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,10 @@ "fmt": "./scripts/fmt" }, "devDependencies": { - "@github/copilot": "^1.0.44", + "@github/copilot": "^1.0.54", "@types/node": "^25.6.2", "esbuild": "^0.28.0", "nrepl-client": "^0.3.0", - "opencode-ai": "^1.15.4" + "opencode-ai": "^1.15.11" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa7c8ea720..8d8107a7af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@github/copilot': - specifier: ^1.0.44 - version: 1.0.44 + specifier: ^1.0.54 + version: 1.0.54 '@types/node': specifier: ^25.6.2 version: 25.6.2 @@ -21,8 +21,8 @@ importers: specifier: ^0.3.0 version: 0.3.0 opencode-ai: - specifier: ^1.15.4 - version: 1.15.4 + specifier: ^1.15.11 + version: 1.15.11 packages: @@ -182,44 +182,60 @@ packages: cpu: [x64] os: [win32] - '@github/copilot-darwin-arm64@1.0.44': - resolution: {integrity: sha512-9NqA5sT2spmNsehxhs51GhXRZIZga5nq+WcMl4LG2QrUPJRDwvHf1bDKqETJUBbYvBY8jONGuTKMRofkMI68YQ==} + '@github/copilot-darwin-arm64@1.0.54': + resolution: {integrity: sha512-ZRiKkxCvDccdGSNB/gmge4UkqMsWWZNIOr0pcim4/x2YUdHbh9cex9RZRjEMXijtUkBTzW5DP/cACuoAqTCyEg==} cpu: [arm64] os: [darwin] hasBin: true - '@github/copilot-darwin-x64@1.0.44': - resolution: {integrity: sha512-QPD8KtXx07SIKILGBl4JDhPyL2Qo0FMmaTYVxR6nkyHkHnFPsUZD6VWGR+T/KMLkcUXFM85Xc1ba9Y27s4nRrQ==} + '@github/copilot-darwin-x64@1.0.54': + resolution: {integrity: sha512-DGqs8x5r4y+SebMco890lNsPrqe6L4v2hCmV1IQ1pvYPvD1o1NMVSZPAQhkdvUeR5bqusOg8+0ugIZOQGTFpFQ==} cpu: [x64] os: [darwin] hasBin: true - '@github/copilot-linux-arm64@1.0.44': - resolution: {integrity: sha512-Z8ScIUP433xS18f68NP9jM9zW320Xzpi2wf7Nig/VyfrwupBy25UTezydQMT0KQHLWTEleHOPcYnASY3HgJXnQ==} + '@github/copilot-linux-arm64@1.0.54': + resolution: {integrity: sha512-waVKu6RuG8YBvCoGrOgtsOxmnfLaUywvbqZXRgvMya1m4akRkMi5r9B2UDr3+egjChp+FIUJVbGIoXN6ZST0rQ==} cpu: [arm64] os: [linux] + libc: [glibc] hasBin: true - '@github/copilot-linux-x64@1.0.44': - resolution: {integrity: sha512-KUl6lvJt0HNKaXSx0T0bIWJ3rvrGwgZYMlkDfqMbuMnZatEQJbjPwxmL/IDfp/c0DyKd7K+ajl17wHYcN/hJIQ==} + '@github/copilot-linux-x64@1.0.54': + resolution: {integrity: sha512-u/ltZa+HDIuhMivkIwkkuylRdEMk5Lp0XjE9w/OityW+BPKjZ+VKAmJ1/1Xm/uUx1IUlZaE3TJnka52wVNOD0A==} cpu: [x64] os: [linux] + libc: [glibc] hasBin: true - '@github/copilot-win32-arm64@1.0.44': - resolution: {integrity: sha512-JVJxZJwAc95ZfapgOXjNFwSqrWlvC3heo128L+CDkdZ6lwpD1dTGMHT/6rMMEeo3xjZmMm8tiynfwsHLDgTtvQ==} + '@github/copilot-linuxmusl-arm64@1.0.54': + resolution: {integrity: sha512-21LLjoQnD57Y1fvO56G1FGVbkt/ffZNDpHqVe2NW7C4r78Gn0hOTqwp+xWRUMpdmxrGZyKeFjX8jK6qox2uF5w==} + cpu: [arm64] + os: [linux] + libc: [musl] + hasBin: true + + '@github/copilot-linuxmusl-x64@1.0.54': + resolution: {integrity: sha512-sbeATKa9vaIetsY1vhQJO0PN/5FgoK48wkGBWCy4BpO8ER/kGYczT22qv6n96gBYrVmC2IZuTFTM4GFpC3bbBw==} + cpu: [x64] + os: [linux] + libc: [musl] + hasBin: true + + '@github/copilot-win32-arm64@1.0.54': + resolution: {integrity: sha512-muOX8qrJSi56BWQejkH0TgXpZYRO8Y9k1qIfMuRojZyLyATn1P4lIKb67ZqDCXJLkcPfVJ5eJYsSAeGwU3Qpww==} cpu: [arm64] os: [win32] hasBin: true - '@github/copilot-win32-x64@1.0.44': - resolution: {integrity: sha512-Yj3KQ/DqwS50PwRtyQITX2mWIVZeJeX+y0faVSMwUUzG1qxmMcme7wimhKOyc4LSV11DpgVB9MSiBw2xys7iww==} + '@github/copilot-win32-x64@1.0.54': + resolution: {integrity: sha512-BheXmqrYFmfRXA0iveKkjKks/2wgK5glrEOARomzy3JCbvVMSPIE8YeK+3YysiOh2SUkWjahwJc09cxaBq4+qQ==} cpu: [x64] os: [win32] hasBin: true - '@github/copilot@1.0.44': - resolution: {integrity: sha512-wr/GmNOUaJK/giJK5abyB1oTpEowgFKLi+NJnlyAymKiK/GKCaRlJqiX23H2RetM8vD2hDYUFUFm9lTCooGy0g==} + '@github/copilot@1.0.54': + resolution: {integrity: sha512-gxiWEQFWxJ3J2Rh67CxKEfER/zayB1z2qaSBUz3RZ0u1iDNJdGPry/1vOQ72X/yHmpGNm+9egucN5VMzyedsIg==} hasBin: true '@types/node@25.6.2': @@ -228,6 +244,10 @@ packages: bencode@2.0.3: resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} @@ -236,69 +256,69 @@ packages: nrepl-client@0.3.0: resolution: {integrity: sha512-EcROXUrzlGHKOdu/E/5WB0OESCI0iGHhdXeYk9cULYtd72eFJrM/Q1umvjTBfKWlT62y76cnyLG/3CmSCqT12w==} - opencode-ai@1.15.4: - resolution: {integrity: sha512-Z5PeJwFNUW4sFW+jYHQTnJm6858dECvWOATvnG0S66Nn46zwjaaZJEJMKQEPOW7Yog99j6k7xRKvJPPAjKDXfQ==} + opencode-ai@1.15.11: + resolution: {integrity: sha512-i3DYIATyWT3ukP+5OCyEuXvbCEq7PgBAlVA4yp01+W5BaYeoj/f0bpXdDPP5q9B/Yl8drtyEhWh0YC9UAHF06A==} cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true - opencode-darwin-arm64@1.15.4: - resolution: {integrity: sha512-d+0sFAAhrDtjQbxRZvYSzy3g/xj/xUDRWUVBWGfkJAx0QEWc/v7cksmnYj3p3l88Gxm/rWeLCh6H32pw1/En8A==} + opencode-darwin-arm64@1.15.11: + resolution: {integrity: sha512-XuiTIkBj0YKpfT8KHNan4JaX686vROCwXQHDzsZ55g/I7rdyQXG2wdt2CsUhRDaPyQTOkhrM+VqC3uYsT+kZzw==} cpu: [arm64] os: [darwin] - opencode-darwin-x64-baseline@1.15.4: - resolution: {integrity: sha512-Lj9wsEPFyEOgLO6J3DsCXdSu/IAJnW/RjtD1oojAao6uHvhs5uXyj1mrsmK8GrtAfCT4JUh8W38o3YYXGjItSw==} + opencode-darwin-x64-baseline@1.15.11: + resolution: {integrity: sha512-itb1FRgGyve89/W+sQBqmTVWUoldb0TdH1Qm805c52UbY+nkEW6oE40vveHJEDDPxFHypyzSymYFcJ7wqYBisw==} cpu: [x64] os: [darwin] - opencode-darwin-x64@1.15.4: - resolution: {integrity: sha512-H412BUw5O+bmXfzLo6UMCWVc3DOYEM0RCI5Kt+Ynqh+Q9878bXK6mwRR7VXgGVBkkH2U4GtT1uDgY0BzSK185Q==} + opencode-darwin-x64@1.15.11: + resolution: {integrity: sha512-PimsC+uaSmVxszQ3sbrIEjDoba9jUyAwAbydEukY3EoQ7cgLFd8tn9H/8yeA0aY7bh3uiCttDCctoQfMpV+S9Q==} cpu: [x64] os: [darwin] - opencode-linux-arm64-musl@1.15.4: - resolution: {integrity: sha512-TO2IVSoYolGKJahf73/hRsJBGxLKOdP/akYPzI0hQQvW4oVrmQkZ3s13jU1+LXIEn4Zbj/TB18QvLzvXrnrEhA==} + opencode-linux-arm64-musl@1.15.11: + resolution: {integrity: sha512-wLaAM12H0mH93XpdXuFz4+oeNA9+CDj8WEvdL8NNrz/Sfgi/zWIo8g3UMH+lp+pfO0s7PTM84RzxGQTOvcejXg==} cpu: [arm64] os: [linux] - opencode-linux-arm64@1.15.4: - resolution: {integrity: sha512-V+x/u9JnPOLPEfqbePSCL0OQdin5gs1V35VsVxj19WaZDEwxlMVjOe6HjVKEY64/O6htkPxCCZohmnMU4dVBMQ==} + opencode-linux-arm64@1.15.11: + resolution: {integrity: sha512-SM+xMU8pUd5p6KD2wdIR2d0q3IRw6axKSGUqlcVrit7ZhFlTjdr3Ca0KqVv3JsUny8Bu8N8Z5b3MHvqRf4nTSQ==} cpu: [arm64] os: [linux] - opencode-linux-x64-baseline-musl@1.15.4: - resolution: {integrity: sha512-xOJ3aHg2+2GrT9F/KmAF0JLB1D6K3SCY/626n+fLjs/AEFvLdmE3TYhoXPEyGH2I9F4kF+4p2xk0pg2b+LVlZQ==} + opencode-linux-x64-baseline-musl@1.15.11: + resolution: {integrity: sha512-N+geBY9Zv2kfoMKYMnPxJZQ1R9xOrgA55BMa7aMtMHr2x9tkjI5mCT7ese6Igna/cnoicxo216YhkSzyY1+p9A==} cpu: [x64] os: [linux] - opencode-linux-x64-baseline@1.15.4: - resolution: {integrity: sha512-dTlV8tAVN8nFdPb7527GR6/BpyIVavAcXJmZ2VbS1daXu4C6k6bpmjiS/ZFKlphRZiKKiEzFrHlimao4BMchVQ==} + opencode-linux-x64-baseline@1.15.11: + resolution: {integrity: sha512-MX+eaLOkFVO5IA8jA0QLUJma3KBwRzUrzZrCFuv6+vq9U/SsD+F9Jz2Bnfw9iE9v3QwnJ+8Yf/vKCsb5LrrWvQ==} cpu: [x64] os: [linux] - opencode-linux-x64-musl@1.15.4: - resolution: {integrity: sha512-IbMaM6zrakdtDD55GUhlT/WeXomXmKsVqo3XQuOaGXprBg3W5alsxXh60SZpV3ftbdcMD/eiB/PYtN/ZN8Fa5w==} + opencode-linux-x64-musl@1.15.11: + resolution: {integrity: sha512-/xbhh8aDFR5E8Ggc00ZG3qXXAwWxosEfWaPXiP04/Y7kDbz8T28a1cSIWniNmY426rYdnYLXgwJzOgpD/ZhDDg==} cpu: [x64] os: [linux] - opencode-linux-x64@1.15.4: - resolution: {integrity: sha512-2c20aldKLfNkg6N6nABvvK1fuaCwYLo/HNeL8ikellkFMeGalCGDhkL/VQ8R8KPV3ohVZJtZwG0nkFiA8MeHCg==} + opencode-linux-x64@1.15.11: + resolution: {integrity: sha512-RuXo1XFiAqW6ypnP4V+rPDmTrdDKh8FuO+3whMpw2qIYs8eVil72QwX1Rv6W96FDCFQkyUZW9R6MK/MGbGWCUQ==} cpu: [x64] os: [linux] - opencode-windows-arm64@1.15.4: - resolution: {integrity: sha512-kr3nIWmYH7NC0Vgrhgjp9EmCuy5MuxjIRrSjzlfRLMaML6U/a0Hsr3AahBwI1KjT+HEhz5u6xpodZeeEDY3nPQ==} + opencode-windows-arm64@1.15.11: + resolution: {integrity: sha512-sdWtLGq1aoaCzbTQY8NR7+g56lzYREBBcT7Na81FKg4H/ZybLQHXV+lwbaw+cK/d0aPpM8EAB+TV6Wbe5nEzGw==} cpu: [arm64] os: [win32] - opencode-windows-x64-baseline@1.15.4: - resolution: {integrity: sha512-2/elQ163r4Q97bYJRrY09IG+bpqh0AKpfutDGCaokFdLWIWQN/cFvjzb4C+BKzLFsU9LRfoyvPhe4nXMm1+S4A==} + opencode-windows-x64-baseline@1.15.11: + resolution: {integrity: sha512-Lj9TkJIeUD++idJWIKK5z+k5hvNubAEXItdxzBLM8LlWArSd2tXCGbxbBktsM5URlhFBdAN05ghyiUtAVOcMPg==} cpu: [x64] os: [win32] - opencode-windows-x64@1.15.4: - resolution: {integrity: sha512-f6p40u3yLEbiq4pzBOXAwtW/NP/dL8uTurHfraPcfezA4ua5DEm4vSoSePUY0CHtubUPuDe0wRUA1s53sysjPQ==} + opencode-windows-x64@1.15.11: + resolution: {integrity: sha512-d5jJqLA+d2DmbEzVrriePxoOjLfqjKZar0OYkNgvmUcWHCHeyn1NcL5JH6T7L19s/X3qY1tEDvZZ3uWFqNMdGQ==} cpu: [x64] os: [win32] @@ -389,32 +409,42 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@github/copilot-darwin-arm64@1.0.44': + '@github/copilot-darwin-arm64@1.0.54': optional: true - '@github/copilot-darwin-x64@1.0.44': + '@github/copilot-darwin-x64@1.0.54': optional: true - '@github/copilot-linux-arm64@1.0.44': + '@github/copilot-linux-arm64@1.0.54': optional: true - '@github/copilot-linux-x64@1.0.44': + '@github/copilot-linux-x64@1.0.54': optional: true - '@github/copilot-win32-arm64@1.0.44': + '@github/copilot-linuxmusl-arm64@1.0.54': optional: true - '@github/copilot-win32-x64@1.0.44': + '@github/copilot-linuxmusl-x64@1.0.54': optional: true - '@github/copilot@1.0.44': + '@github/copilot-win32-arm64@1.0.54': + optional: true + + '@github/copilot-win32-x64@1.0.54': + optional: true + + '@github/copilot@1.0.54': + dependencies: + detect-libc: 2.1.2 optionalDependencies: - '@github/copilot-darwin-arm64': 1.0.44 - '@github/copilot-darwin-x64': 1.0.44 - '@github/copilot-linux-arm64': 1.0.44 - '@github/copilot-linux-x64': 1.0.44 - '@github/copilot-win32-arm64': 1.0.44 - '@github/copilot-win32-x64': 1.0.44 + '@github/copilot-darwin-arm64': 1.0.54 + '@github/copilot-darwin-x64': 1.0.54 + '@github/copilot-linux-arm64': 1.0.54 + '@github/copilot-linux-x64': 1.0.54 + '@github/copilot-linuxmusl-arm64': 1.0.54 + '@github/copilot-linuxmusl-x64': 1.0.54 + '@github/copilot-win32-arm64': 1.0.54 + '@github/copilot-win32-x64': 1.0.54 '@types/node@25.6.2': dependencies: @@ -422,6 +452,8 @@ snapshots: bencode@2.0.3: {} + detect-libc@2.1.2: {} + esbuild@0.28.0: optionalDependencies: '@esbuild/aix-ppc64': 0.28.0 @@ -456,55 +488,55 @@ snapshots: bencode: 2.0.3 tree-kill: 1.2.2 - opencode-ai@1.15.4: + opencode-ai@1.15.11: optionalDependencies: - opencode-darwin-arm64: 1.15.4 - opencode-darwin-x64: 1.15.4 - opencode-darwin-x64-baseline: 1.15.4 - opencode-linux-arm64: 1.15.4 - opencode-linux-arm64-musl: 1.15.4 - opencode-linux-x64: 1.15.4 - opencode-linux-x64-baseline: 1.15.4 - opencode-linux-x64-baseline-musl: 1.15.4 - opencode-linux-x64-musl: 1.15.4 - opencode-windows-arm64: 1.15.4 - opencode-windows-x64: 1.15.4 - opencode-windows-x64-baseline: 1.15.4 + opencode-darwin-arm64: 1.15.11 + opencode-darwin-x64: 1.15.11 + opencode-darwin-x64-baseline: 1.15.11 + opencode-linux-arm64: 1.15.11 + opencode-linux-arm64-musl: 1.15.11 + opencode-linux-x64: 1.15.11 + opencode-linux-x64-baseline: 1.15.11 + opencode-linux-x64-baseline-musl: 1.15.11 + opencode-linux-x64-musl: 1.15.11 + opencode-windows-arm64: 1.15.11 + opencode-windows-x64: 1.15.11 + opencode-windows-x64-baseline: 1.15.11 - opencode-darwin-arm64@1.15.4: + opencode-darwin-arm64@1.15.11: optional: true - opencode-darwin-x64-baseline@1.15.4: + opencode-darwin-x64-baseline@1.15.11: optional: true - opencode-darwin-x64@1.15.4: + opencode-darwin-x64@1.15.11: optional: true - opencode-linux-arm64-musl@1.15.4: + opencode-linux-arm64-musl@1.15.11: optional: true - opencode-linux-arm64@1.15.4: + opencode-linux-arm64@1.15.11: optional: true - opencode-linux-x64-baseline-musl@1.15.4: + opencode-linux-x64-baseline-musl@1.15.11: optional: true - opencode-linux-x64-baseline@1.15.4: + opencode-linux-x64-baseline@1.15.11: optional: true - opencode-linux-x64-musl@1.15.4: + opencode-linux-x64-musl@1.15.11: optional: true - opencode-linux-x64@1.15.4: + opencode-linux-x64@1.15.11: optional: true - opencode-windows-arm64@1.15.4: + opencode-windows-arm64@1.15.11: optional: true - opencode-windows-x64-baseline@1.15.4: + opencode-windows-x64-baseline@1.15.11: optional: true - opencode-windows-x64@1.15.4: + opencode-windows-x64@1.15.11: optional: true tree-kill@1.2.2: {}