Compare commits

..

15 Commits

Author SHA1 Message Date
Andrey Antukh
4a7b89a1da Merge pull request #8327 from penpot/niwinz-develop-rlimit-notifications
 Add proper mattermost notifications for rlimit rejects
2026-02-13 17:11:54 +01:00
David Barragán Merino
cc28bd44f6 🔧 Fix the plugin style documentation build command 2026-02-13 14:18:35 +01:00
David Barragán Merino
fe833c9e34 🔧 Disable observability for plugin docs and packages
This reverts commit a4f2641cc9.
2026-02-13 13:55:13 +01:00
Alejandro Alonso
8d225af13a Merge pull request #8351 from penpot/alotor-fix-create-rect-click
🐛 Fix problem when create click
2026-02-13 13:21:27 +01:00
Juanfran
449aa65f8d 🐛 Fix e2e tests for plugins 2026-02-13 13:17:08 +01:00
Andrey Antukh
bd7f4dca3a 🐛 Fix rpc methods on plugins e2e tests 2026-02-13 13:17:08 +01:00
Andrey Antukh
1e7bef081a Allow self-signed certs on plugins e2e browser setup 2026-02-13 13:17:08 +01:00
Andrey Antukh
12bc3ac9ed Update default cors headers 2026-02-13 13:17:08 +01:00
alonso.torres
3ea0a781f1 🐛 Fix problem when create click 2026-02-13 12:38:33 +01:00
Sagar
cfcebf59d5 🐛 Make S3Client and S3Presigner use identical credential resolution (#8316) 2026-02-13 12:21:05 +01:00
Andrey Antukh
cf43ac23a1 Merge pull request #8340 from penpot/hiru-fix-plugins-api-tokens
🐛 Fix problems about applying tokens to shapes with plugins
2026-02-13 12:18:29 +01:00
David Barragán Merino
fda09b02b9 🔧 Fix the plugin bundle build command 2026-02-13 09:37:22 +01:00
Andrés Moya
a23ca6a1cb 🐛 Fix applied tokens reading in shape proxy 2026-02-12 17:14:16 +01:00
Andrés Moya
c626634610 🐛 Detect empty font-family 2026-02-12 16:04:23 +01:00
Andrés Moya
11eedd0368 🐛 Patch alternative ways of applying tokens to shapes 2026-02-12 16:01:55 +01:00
43 changed files with 750 additions and 622 deletions

View File

@@ -80,7 +80,7 @@ jobs:
- name: "Build package for ${{ inputs.plugin_name }}-plugin" - name: "Build package for ${{ inputs.plugin_name }}-plugin"
working-directory: plugins working-directory: plugins
shell: bash shell: bash
run: npx nx build ${{ inputs.plugin_name }}-plugin run: pnpm --filter ${{ inputs.plugin_name }}-plugin build
- name: Select Worker name - name: Select Worker name
run: | run: |

View File

@@ -78,7 +78,7 @@ jobs:
- name: Build styles - name: Build styles
working-directory: plugins working-directory: plugins
shell: bash shell: bash
run: npx nx run example-styles:build run: pnpm run build:styles-example
- name: Select Worker name - name: Select Worker name
run: | run: |

View File

@@ -12,6 +12,7 @@ penpot - error list
<a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a> <a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a>
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a> <a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a>
<a class="{% if version = 5 %}strong{% endif %}" href="?version=5">[RLIMIT REPORTS]</a>
</div> </div>
</nav> </nav>
<main class="horizontal-list"> <main class="horizontal-list">

View File

@@ -0,0 +1,40 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Rate Limit Report
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<div>[<a href="#context">context</a>]</div>
<div>[<a href="#result">result</a>]</div>
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="head" class="table-key">HEAD:</div>
<div class="table-val">
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
<div class="table-row multiline">
<div id="result" class="table-key">RESULT: </div>
<div class="table-val">
<pre>{{result}}</pre>
</div>
</div>
</div>
</main>
{% endblock %}

View File

@@ -3,9 +3,9 @@
{:default {:default
[[:default :window "200000/h"]] [[:default :window "200000/h"]]
;; #{:command/get-teams} ;; #{:main/get-teams}
;; [[:burst :bucket "5/5/5s"]] ;; [[:burst :bucket "5/5/5s"]]
;; #{:command/get-profile} ;; #{:main/get-profile}
;; [[:burst :bucket "60/60/1m"]] ;; [[:burst :bucket "60/60/1m"]]
} }

View File

@@ -240,6 +240,13 @@
(tmpl/render (-> content (tmpl/render (-> content
(assoc :id id) (assoc :id id)
(assoc :version 4) (assoc :version 4)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
(render-template-v5 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v5.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 5)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))] (assoc :created-at (ct/format-inst created-at :rfc1123))))))]
(if-let [report (get-report request)] (if-let [report (get-report request)]
@@ -247,7 +254,8 @@
1 (render-template-v1 report) 1 (render-template-v1 report)
2 (render-template-v2 report) 2 (render-template-v2 report)
3 (render-template-v3 report) 3 (render-template-v3 report)
4 (render-template-v4 report))] 4 (render-template-v4 report)
5 (render-template-v5 report))]
{::yres/status 200 {::yres/status 200
::yres/body result ::yres/body result
::yres/headers {"content-type" "text/html; charset=utf-8" ::yres/headers {"content-type" "text/html; charset=utf-8"

View File

@@ -213,14 +213,14 @@
(assoc "access-control-allow-origin" origin) (assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH") (assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true") (assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie") (assoc "access-control-expose-headers" "content-type, set-cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))) (assoc "access-control-allow-headers" "x-frontend-version, x-client, x-requested-width, content-type, accept, cookie")))
(defn wrap-cors (defn wrap-cors
[handler] [handler]
(fn [request] (fn [request]
(let [response (if (= (yreq/method request) :options) (let [response (if (= (yreq/method request) :options)
{::yres/status 200} {::yres/status 204}
(handler request)) (handler request))
origin (yreq/get-header request "origin")] origin (yreq/get-header request "origin")]
(update response ::yres/headers with-cors-headers origin)))) (update response ::yres/headers with-cors-headers origin))))

View File

@@ -151,20 +151,22 @@
uuid/zero) uuid/zero)
props (-> (or (::replace-props resultm) props (-> (or (::replace-props resultm)
(-> params (merge params (::props resultm)))
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props)) (clean-props))
context (merge (::context resultm) context (merge (::context resultm)
(prepare-context-from-request request)) (prepare-context-from-request request))
ip-addr (inet/parse-request request)] ip-addr (inet/parse-request request)
module (get cfg ::rpc/module)]
{::type (or (::type resultm) {::type (or (::type resultm)
(::rpc/type cfg)) (::rpc/type cfg))
::name (or (::name resultm) ::name (or (::name resultm)
(::sv/name mdata)) (let [sname (::sv/name mdata)]
(if (not= module "main")
(str module "-" sname)
sname)))
::profile-id profile-id ::profile-id profile-id
::ip-addr ip-addr ::ip-addr ip-addr
::props props ::props props

View File

@@ -15,6 +15,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px] [promesa.exec :as px]
@@ -41,7 +42,7 @@
(or (instance? java.util.concurrent.CompletionException cause) (or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))) (instance? java.util.concurrent.ExecutionException cause)))
(defn record->report (defn- log-record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}] [{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record") (assert (l/valid-record? record) "expectd valid log record")
(let [data (if (concurrent-exception? cause) (let [data (if (concurrent-exception? cause)
@@ -86,16 +87,16 @@
[{:keys [::db/pool]} {:keys [::l/id] :as record}] [{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try (try
(let [uri (cf/get :public-uri) (let [uri (cf/get :public-uri)
report (-> record record->report d/without-nils)] report (-> record log-record->report d/without-nils)]
(l/dbg :hint "registering error on database" (l/dbg :hint "registering error on database"
:id id :id (str id)
:src "logging" :src "logging"
:uri (str uri "/dbg/error/" id)) :uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 3 report)) (persist-on-database! pool id 3 report))
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause)))) (l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- event->report (defn- audit-event->report
[{:keys [::audit/context ::audit/props ::audit/ip-addr] :as record}] [{:keys [::audit/context ::audit/props ::audit/ip-addr] :as record}]
(let [context (let [context
(reduce-kv (fn [context k v] (reduce-kv (fn [context k v]
@@ -125,15 +126,51 @@
[{:keys [::db/pool]} {:keys [::audit/id] :as event}] [{:keys [::db/pool]} {:keys [::audit/id] :as event}]
(try (try
(let [uri (cf/get :public-uri) (let [uri (cf/get :public-uri)
report (-> event event->report d/without-nils)] report (-> event audit-event->report d/without-nils)]
(l/dbg :hint "registering error on database" (l/dbg :hint "registering error on database"
:id id :id (str id)
:src "audit-log" :src "audit-log"
:uri (str uri "/dbg/error/" id)) :uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 4 report)) (persist-on-database! pool id 4 report))
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause)))) (l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- rlimit-event->report
[event]
(let [context
(-> {}
(assoc :rlimit/uid (::rlimit/uid event))
(assoc :rlimit/method (::rlimit/method event))
(assoc :backend/tenant (cf/get :tenant))
(assoc :backend/host (cf/get :host))
(assoc :backend/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version)))
result
(->> (::rlimit/results event)
(mapv (fn [result]
(-> (into (sorted-map) result)
(dissoc ::rlimit/method)))))]
{:hint (str "Rate Limit Rejection: " (::rlimit/method event) " for " (::rlimit/uid event))
:context (-> (into (sorted-map) context)
(pp/pprint-str :length 50))
:result (pp/pprint-str result :length 50)}))
(defn- handle-rlimit-event
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [::rlimit/id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event rlimit-event->report d/without-nils)]
(l/dbg :hint "registering rate limit rejection"
:id (str id)
:src "rlimit"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 5 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defmethod ig/assert-key ::reporter (defmethod ig/assert-key ::reporter
[_ params] [_ params]
(assert (db/pool? (::db/pool params)) "expect valid database pool")) (assert (db/pool? (::db/pool params)) "expect valid database pool"))
@@ -154,6 +191,9 @@
(::audit/id item) (::audit/id item)
(handle-audit-event cfg item) (handle-audit-event cfg item)
(::rlimit/id item)
(handle-rlimit-event cfg item)
:else :else
(l/warn :hint "received unexpected item" :item item)) (l/warn :hint "received unexpected item" :item item))

View File

@@ -9,10 +9,12 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
[app.http.client :as http] [app.http.client :as http]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[app.util.json :as json] [app.util.json :as json]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px] [promesa.exec :as px]
@@ -22,21 +24,28 @@
(defn- send-mattermost-notification! (defn- send-mattermost-notification!
[cfg {:keys [id] :as report}] [cfg {:keys [id] :as report}]
(let [type (get report :type)
text (str "#" type " | " (get report :hint) "\n"
(when id
(str (u/join (cf/get :public-uri) "/dbg/error/" id) " "))
(let [url (u/join (cf/get :public-uri) "/dbg/error/" id)
text (str "Exception: " url " "
(when-let [pid (:profile-id report)] (when-let [pid (:profile-id report)]
(str "(pid: #uuid-" pid ")")) (if (uuid? pid)
(str "(pid: #uuid-" pid ")")
(str "(pid: #ip-" pid ")")))
"\n" "\n"
"- host: #" (:host report) "\n" "- host: #" (:host report) "\n"
"- tenant: #" (:tenant report) "\n" "- tenant: #" (:tenant report) "\n"
"- origin: #" (:origin report) "\n" "- origin: #" (:origin report) "\n"
"- href: `" (:href report) "`\n" (when-let [href (get report :href)]
"- frontend-version: `" (:frontend-version report) "`\n" (str "- href: `" href "`\n"))
"- backend-version: `" (:backend-version report) "`\n" (when-let [version (get report :frontend-version)]
(str "- frontend-version: `" version "`\n"))
(when-let [version (get report :backend-version)]
(str "- backend-version: `" version "`\n"))
"\n" "\n"
(when-let [info (:info report)]
(str "```\n" info "```"))
(when-let [trace (:trace report)] (when-let [trace (:trace report)]
(str "```\n" (str "```\n"
"Trace:\n" "Trace:\n"
@@ -54,13 +63,15 @@
(l/warn :hint "error on sending data" (l/warn :hint "error on sending data"
:response (pr-str resp))))) :response (pr-str resp)))))
(defn- record->report (defn- log-record->report
[{:keys [::l/context ::l/id ::l/cause] :as record}] [{:keys [::l/context ::l/id ::l/cause ::l/message] :as record}]
(assert (l/valid-record? record) "expectd valid log record") (assert (l/valid-record? record) "expectd valid log record")
(let [public-uri (cf/get :public-uri)] (let [public-uri (cf/get :public-uri)]
{:id id {:id id
:type "exception"
:origin "logging" :origin "logging"
:hint (or (some-> cause ex-message) @message)
:tenant (cf/get :tenant) :tenant (cf/get :tenant)
:host (cf/get :host) :host (cf/get :host)
:backend-version (:full cf/version) :backend-version (:full cf/version)
@@ -74,7 +85,9 @@
(defn- audit-event->report (defn- audit-event->report
[{:keys [::audit/context ::audit/props ::audit/id] :as event}] [{:keys [::audit/context ::audit/props ::audit/id] :as event}]
{:id id {:id id
:type "exception"
:origin "audit-log" :origin "audit-log"
:hint (get props :hint)
:tenant (cf/get :tenant) :tenant (cf/get :tenant)
:host (cf/get :host) :host (cf/get :host)
:backend-version (:full cf/version) :backend-version (:full cf/version)
@@ -82,18 +95,35 @@
:profile-id (:audit/profile-id event) :profile-id (:audit/profile-id event)
:href (get props :href)}) :href (get props :href)})
(defn- handle-log-record (defn- rlimit-event->report
[cfg record] [event]
(try {:id (::rlimit/id event)
(let [report (record->report record)] :type "notification"
(send-mattermost-notification! cfg report)) :origin "rlimit"
(catch Throwable cause :hint (str "rlimit reject of "
(l/warn :hint "unhandled error" :cause cause)))) (::rlimit/method event)
" for "
(::rlimit/uid event))
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
:profile-id (::rlimit/profile-id event)
:info (with-out-str
(println "Rejected by:")
(println "------------")
(println "Method: " (::rlimit/method event))
(println "Limit Name: " (::rlimit/name event))
(println "Limit Strategy:" (::rlimit/strategy event))
(println)
(println "Results & Config:")
(println "-----------------")
(doseq [result (::rlimit/results event)]
(pp/pprint (into (sorted-map) result))))})
(defn- handle-audit-event (defn- handle-event
[cfg record] [cfg event event->report]
(try (try
(let [report (audit-event->report record)] (let [report (event->report event)]
(send-mattermost-notification! cfg report)) (send-mattermost-notification! cfg report))
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unhandled error" :cause cause)))) (l/warn :hint "unhandled error" :cause cause))))
@@ -116,10 +146,13 @@
(when @enabled (when @enabled
(cond (cond
(::l/id item) (::l/id item)
(handle-log-record cfg item) (handle-event cfg item log-record->report)
(::audit/id item) (::audit/id item)
(handle-audit-event cfg item) (handle-event cfg item audit-event->report)
(::rlimit/id item)
(handle-event cfg item rlimit-event->report)
:else :else
(l/warn :hint "received unexpected item" :item item))) (l/warn :hint "received unexpected item" :item item)))

View File

@@ -317,7 +317,13 @@
::climit/enabled (contains? cf/flags :rpc-climit)} ::climit/enabled (contains? cf/flags :rpc-climit)}
:app.rpc/rlimit :app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/netty-executor)} {::wrk/executor (ig/ref ::wrk/netty-executor)
:app.loggers.mattermost/reporter
(ig/ref :app.loggers.mattermost/reporter)
:app.loggers.database/reporter
(ig/ref :app.loggers.database/reporter)}
:app.rpc/methods :app.rpc/methods
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)

View File

@@ -90,7 +90,7 @@
[methods] [methods]
(let [methods (update-vals methods peek)] (let [methods (update-vals methods peek)]
(fn [{:keys [params path-params method] :as request}] (fn [{:keys [params path-params method] :as request}]
(let [handler-name (:type path-params) (let [handler-name (:method-name path-params)
etag (yreq/get-header request "if-none-match") etag (yreq/get-header request "if-none-match")
key-id (get request ::http/auth-key-id) key-id (get request ::http/auth-key-id)
@@ -227,8 +227,8 @@
(wrap-authentication cfg $ mdata))) (wrap-authentication cfg $ mdata)))
(defn- process-method (defn- process-method
[cfg module wrap-fn [f mdata]] [cfg wrap-fn [f mdata]]
(l/trc :hint "add method" :module module :name (::sv/name mdata)) (l/trc :hint "add method" :module (::module cfg) :type (::type cfg) :name (::sv/name mdata))
(let [f (wrap-fn cfg f mdata) (let [f (wrap-fn cfg f mdata)
k (keyword (::sv/name mdata))] k (keyword (::sv/name mdata))]
[k [mdata (partial f cfg)]])) [k [mdata (partial f cfg)]]))
@@ -239,7 +239,7 @@
(defn- resolve-methods (defn- resolve-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] (let [cfg (assoc cfg ::module "main" ::type "command" ::metrics-id :rpc-main-timing)]
(->> (sv/scan-ns (->> (sv/scan-ns
'app.rpc.commands.access-token 'app.rpc.commands.access-token
'app.rpc.commands.audit 'app.rpc.commands.audit
@@ -266,7 +266,7 @@
'app.rpc.commands.verify-token 'app.rpc.commands.verify-token
'app.rpc.commands.viewer 'app.rpc.commands.viewer
'app.rpc.commands.webhooks) 'app.rpc.commands.webhooks)
(map (partial process-method cfg "rpc" wrap)) (map (partial process-method cfg wrap))
(into {})))) (into {}))))
(def ^:private schema:methods-params (def ^:private schema:methods-params
@@ -298,13 +298,13 @@
(defn- resolve-management-methods (defn- resolve-management-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing) (let [cfg (assoc cfg ::module "management" ::type "command" ::metrics-id :rpc-management-timing)
mods (cond->> (list 'app.rpc.management.exporter) mods (cond->> (list 'app.rpc.management.exporter)
(contains? cf/flags :nitrate) (contains? cf/flags :nitrate)
(cons 'app.rpc.management.nitrate))] (cons 'app.rpc.management.nitrate))]
(->> (apply sv/scan-ns mods) (->> (apply sv/scan-ns mods)
(map (partial process-method cfg "management" wrap-management)) (map (partial process-method cfg wrap-management))
(into {})))) (into {}))))
(def ^:private schema:management-methods-params (def ^:private schema:management-methods-params
@@ -359,7 +359,7 @@
(let [public-uri (cf/get :public-uri)] (let [public-uri (cf/get :public-uri)]
["/api" ["/api"
["/management" ["/management"
["/methods/:type" ["/methods/:method-name"
{:middleware [[mw/shared-key-auth shared-keys] {:middleware [[mw/shared-key-auth shared-keys]
[session/authz cfg]] [session/authz cfg]]
:handler (make-rpc-handler management-methods)}] :handler (make-rpc-handler management-methods)}]
@@ -370,7 +370,7 @@
:description "MANAGEMENT API")] :description "MANAGEMENT API")]
["/main" ["/main"
["/methods/:type" ["/methods/:method-name"
{:middleware [[mw/cors] {:middleware [[mw/cors]
[sec/client-header-check] [sec/client-header-check]
[session/authz cfg] [session/authz cfg]
@@ -388,7 +388,7 @@
["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}] ["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}]
["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}] ["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}]
["/rpc/command/:type" ["/rpc/command/:method-name"
{:middleware [[mw/cors] {:middleware [[mw/cors]
[sec/client-header-check] [sec/client-header-check]
[session/authz cfg] [session/authz cfg]

View File

@@ -52,6 +52,8 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.http :as-alias http] [app.http :as-alias http]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.redis :as rds] [app.redis :as rds]
[app.redis.script :as-alias rscript] [app.redis.script :as-alias rscript]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
@@ -171,9 +173,9 @@
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))) :hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket (defmethod process-limit :bucket
[rconn profile-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}] [rconn profile-id now {:keys [::key ::params ::method ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script (let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." profile-id)]) (assoc ::rscript/keys [(str key "." method "." profile-id)])
(assoc ::rscript/vals (conj params (->seconds now)))) (assoc ::rscript/vals (conj params (->seconds now))))
result (rds/eval rconn script) result (rds/eval rconn script)
allowed? (boolean (nth result 0)) allowed? (boolean (nth result 0))
@@ -181,7 +183,7 @@
reset (* (/ (inst-ms interval) rate) reset (* (/ (inst-ms interval) rate)
(- capacity remaining))] (- capacity remaining))]
(l/trace :hint "limit processed" (l/trace :hint "limit processed"
:service service :method method
:limit (name (::name limit)) :limit (name (::name limit))
:strategy (name (::strategy limit)) :strategy (name (::strategy limit))
:opts (::opts limit) :opts (::opts limit)
@@ -193,17 +195,17 @@
(assoc ::lresult/remaining remaining)))) (assoc ::lresult/remaining remaining))))
(defmethod process-limit :window (defmethod process-limit :window
[rconn profile-id now {:keys [::permits ::unit ::key ::service] :as limit}] [rconn uid now {:keys [::permits ::unit ::key ::method] :as limit}]
(let [ts (ct/truncate now unit) (let [ts (ct/truncate now unit)
ttl (ct/diff now (ct/plus ts {unit 1})) ttl (ct/diff now (ct/plus ts {unit 1}))
script (-> window-rate-limit-script script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." service "." profile-id "." (ct/format-inst ts))]) (assoc ::rscript/keys [(str key "." method "." uid "." (ct/format-inst ts))])
(assoc ::rscript/vals [permits (->seconds ttl)])) (assoc ::rscript/vals [permits (->seconds ttl)]))
result (rds/eval rconn script) result (rds/eval rconn script)
allowed? (boolean (nth result 0)) allowed? (boolean (nth result 0))
remaining (nth result 1)] remaining (nth result 1)]
(l/trace :hint "limit processed" (l/trace :hint "limit processed"
:service service :method method
:name (name (::name limit)) :name (name (::name limit))
:strategy (name (::strategy limit)) :strategy (name (::strategy limit))
:opts (::opts limit) :opts (::opts limit)
@@ -211,12 +213,13 @@
:remaining remaining) :remaining remaining)
(-> limit (-> limit
(assoc ::lresult/allowed allowed?) (assoc ::lresult/allowed allowed?)
(assoc ::lresult/timestamp ts)
(assoc ::lresult/remaining remaining) (assoc ::lresult/remaining remaining)
(assoc ::lresult/reset (ct/plus ts {unit 1}))))) (assoc ::lresult/reset (ct/plus ts {unit 1})))))
(defn- process-limits (defn- process-limits
[rconn profile-id limits now] [{:keys [::rds/conn] :as cfg} uid limits now]
(let [results (into [] (map (partial process-limit rconn profile-id now)) limits) (let [results (into [] (map (partial process-limit conn uid now)) limits)
remaining (->> results remaining (->> results
(d/index-by ::name ::lresult/remaining) (d/index-by ::name ::lresult/remaining)
(uri/map->query-string)) (uri/map->query-string))
@@ -227,11 +230,22 @@
rejected (d/seek (complement ::lresult/allowed) results)] rejected (d/seek (complement ::lresult/allowed) results)]
(when rejected (when rejected
(l/warn :hint "rejected rate limit" (let [event {::id (uuid/next)
:profile-id (str profile-id) ::uid uid
:limit-service (-> rejected ::service name) ::method (-> rejected ::method name)
:limit-name (-> rejected ::name name) ::name (-> rejected ::name name)
:limit-strategy (-> rejected ::strategy name))) ::strategy (-> rejected ::strategy name)
::results results}]
(l/warn :hint "rejected rate limit"
:method (-> rejected ::method name)
:name (-> rejected ::name name)
:strategy (-> rejected ::strategy name)
:uid (str uid)
:report-id (:id event))
(loggers.mm/emit cfg event)
(loggers.db/emit cfg event)))
{::enabled true {::enabled true
::allowed (not (some? rejected)) ::allowed (not (some? rejected))
@@ -244,7 +258,7 @@
[state skey sname] [state skey sname]
(when-let [limits (or (get-in @state [::limits skey]) (when-let [limits (or (get-in @state [::limits skey])
(get-in @state [::limits :default]))] (get-in @state [::limits :default]))]
(into [] (map #(assoc % ::service sname)) limits))) (into [] (map #(assoc % ::method sname)) limits)))
(defn- get-uid (defn- get-uid
[{:keys [::rpc/profile-id] :as params}] [{:keys [::rpc/profile-id] :as params}]
@@ -254,10 +268,10 @@
uuid/zero))) uuid/zero)))
(defn- process-request' (defn- process-request'
[{:keys [::rds/conn] :as cfg} limits params] [cfg limits params]
(try (try
(let [uid (get-uid params) (let [uid (get-uid params)
result (process-limits conn uid limits (ct/now))] result (process-limits cfg uid limits (ct/now))]
(if (contains? cf/flags :soft-rpc-rlimit) (if (contains? cf/flags :soft-rpc-rlimit)
{::enabled false} {::enabled false}
result)) result))
@@ -275,8 +289,8 @@
(assert (or (nil? rlimit) (valid-rlimit-instance? rlimit)) "expected a valid rlimit instance") (assert (or (nil? rlimit) (valid-rlimit-instance? rlimit)) "expected a valid rlimit instance")
(if rlimit (if rlimit
(let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name)) (let [skey (keyword (::rpc/module cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name)) sname (str (::rpc/module cfg) "." (->> mdata ::sv/spec name))
cfg (-> cfg cfg (-> cfg
(assoc ::skey skey) (assoc ::skey skey)
(assoc ::sname sname))] (assoc ::sname sname))]

View File

@@ -33,6 +33,7 @@
java.util.Optional java.util.Optional
java.util.concurrent.atomic.AtomicLong java.util.concurrent.atomic.AtomicLong
org.reactivestreams.Subscriber org.reactivestreams.Subscriber
software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer software.amazon.awssdk.core.async.AsyncResponseTransformer
@@ -199,7 +200,8 @@
(defn- build-s3-client (defn- build-s3-client
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}] [{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
(let [aconfig (-> (ClientAsyncConfiguration/builder) (let [creds-provider (DefaultCredentialsProvider/create)
aconfig (-> (ClientAsyncConfiguration/builder)
(.build)) (.build))
sconfig (-> (S3Configuration/builder) sconfig (-> (S3Configuration/builder)
@@ -221,6 +223,7 @@
builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig) builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient) builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
builder (.region ^S3AsyncClientBuilder builder (lookup-region region)) builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
builder (.credentialsProvider ^S3AsyncClientBuilder builder creds-provider)
builder (cond-> ^S3AsyncClientBuilder builder builder (cond-> ^S3AsyncClientBuilder builder
(some? endpoint) (some? endpoint)
(.endpointOverride (URI. (str endpoint))))] (.endpointOverride (URI. (str endpoint))))]
@@ -237,7 +240,8 @@
(defn- build-s3-presigner (defn- build-s3-presigner
[{:keys [::region ::endpoint]}] [{:keys [::region ::endpoint]}]
(let [config (-> (S3Configuration/builder) (let [creds-provider (DefaultCredentialsProvider/create)
config (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true)) (cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))] (.build))]
@@ -245,6 +249,7 @@
(cond-> (some? endpoint) (.endpointOverride (URI. (str endpoint)))) (cond-> (some? endpoint) (.endpointOverride (URI. (str endpoint))))
(.region (lookup-region region)) (.region (lookup-region region))
(.serviceConfiguration ^S3Configuration config) (.serviceConfiguration ^S3Configuration config)
(.credentialsProvider creds-provider)
(.build)))) (.build))))
(defn- write-input-stream (defn- write-input-stream

View File

@@ -104,13 +104,13 @@
(assoc-in [::db/pool ::db/password] (:database-password config)) (assoc-in [::db/pool ::db/password] (:database-password config))
(assoc-in [:app.rpc/methods :app.setup/templates] templates) (assoc-in [:app.rpc/methods :app.setup/templates] templates)
(assoc-in [:app.rpc/methods :app.setup/templates] templates) (assoc-in [:app.rpc/methods :app.setup/templates] templates)
(update :app.rpc/methods (update :app.rpc/rlimit assoc
(fn [state] :app.loggers.mattermost/reporter nil
(-> state :app.loggers.database/reporter nil)
(assoc :app.setup/templates templates) (update :app.rpc/methods assoc
(assoc :app.loggers.mattermost/reporter nil) :app.setup/templates templates
(assoc :app.loggers.database/reporter nil)))) :app.loggers.mattermost/reporter nil
:app.loggers.database/reporter nil)
(dissoc :app.srepl/server (dissoc :app.srepl/server
:app.http/server :app.http/server
:app.http/route :app.http/route

View File

@@ -35,7 +35,7 @@
[::sm/text {:error/fn token-value-empty-fn}]) [::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-font-family (def schema:token-value-font-family
[:vector :string]) [:vector ::sm/text])
(def schema:token-value-typography-map (def schema:token-value-typography-map
[:map [:map

View File

@@ -43,9 +43,13 @@
(> dy dx) (> dy dx)
(assoc :x (- (:x point) (* sx (- dy dx))))))) (assoc :x (- (:x point) (* sx (- dy dx)))))))
(defn resize-shape [{:keys [x y width height] :as shape} initial point lock? mod?] (defn resize-shape [{:keys [x y width height] :as shape} initial point lock? mod? snap-pixel?]
(if (and (some? x) (some? y) (some? width) (some? height)) (if (and (some? x) (some? y) (some? width) (some? height))
(let [draw-rect (grc/make-rect initial (cond-> point lock? (adjust-ratio initial))) (let [draw-rect (cond-> (grc/make-rect initial (cond-> point lock? (adjust-ratio initial)))
snap-pixel?
(-> (update :width max 1)
(update :height max 1)))
shape-rect (grc/make-rect x y width height) shape-rect (grc/make-rect x y width height)
scalev (gpt/point (/ (:width draw-rect) scalev (gpt/point (/ (:width draw-rect)
@@ -64,8 +68,8 @@
(ctm/move movev))))) (ctm/move movev)))))
shape)) shape))
(defn update-drawing [state initial point lock? mod?] (defn- update-drawing [state initial point lock? mod? snap-pixel?]
(update-in state [:workspace-drawing :object] resize-shape initial point lock? mod?)) (update-in state [:workspace-drawing :object] resize-shape initial point lock? mod? snap-pixel?))
(defn move-drawing (defn move-drawing
[{:keys [x y]}] [{:keys [x y]}]
@@ -120,7 +124,7 @@
(rx/map move-drawing)) (rx/map move-drawing))
(->> ms/mouse-position (->> ms/mouse-position
(rx/filter #(> (gpt/distance % initial) (/ 2 zoom))) (rx/filter #(> (* (gpt/distance % initial) zoom) 10))
;; Take until before the snap calculation otherwise we could cancel the snap in the worker ;; Take until before the snap calculation otherwise we could cancel the snap in the worker
;; and its a problem for fast moving drawing ;; and its a problem for fast moving drawing
(rx/take-until stopper) (rx/take-until stopper)
@@ -131,7 +135,7 @@
(rx/map (partial array/conj current))))) (rx/map (partial array/conj current)))))
(rx/map (rx/map
(fn [[_ shift? mod? point]] (fn [[_ shift? mod? point]]
#(update-drawing % initial (cond-> point snap-pixel? (gpt/round-step 1)) shift? mod?)))))) #(update-drawing % initial (cond-> point snap-pixel? (gpt/round-step 1)) shift? mod? snap-pixel?))))))
(->> (rx/of (common/handle-finish-drawing)) (->> (rx/of (common/handle-finish-drawing))
(rx/delay 100))))))) (rx/delay 100)))))))

View File

@@ -126,6 +126,6 @@
(defn check-permission (defn check-permission
[plugin-id permission] [plugin-id permission]
(or (= plugin-id "TEST") (or (= plugin-id "00000000-0000-0000-0000-000000000000")
(let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])] (let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])]
(contains? permissions permission)))) (contains? permissions permission))))

View File

@@ -11,6 +11,7 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.geom.rect :as grc] [app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.json :as json]
[app.common.path-names :as cpn] [app.common.path-names :as cpn]
[app.common.record :as crc] [app.common.record :as crc]
[app.common.schema :as sm] [app.common.schema :as sm]
@@ -1295,7 +1296,7 @@
(get :applied-tokens))] (get :applied-tokens))]
(reduce (reduce
(fn [acc [prop name]] (fn [acc [prop name]]
(obj/set! acc (d/name prop) name)) (obj/set! acc (json/write-camel-key prop) name))
#js {} #js {}
tokens)))} tokens)))}

View File

@@ -16,7 +16,7 @@
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st] [app.main.store :as st]
[app.plugins.shape :as shape] ;; [app.plugins.shape :as shape]
[app.plugins.utils :as u] [app.plugins.utils :as u]
[app.util.object :as obj] [app.util.object :as obj]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
@@ -113,13 +113,17 @@
:applyToShapes :applyToShapes
{:schema [:tuple {:schema [:tuple
[:vector [:fn shape/shape-proxy?]] ;; FIXME: the schema decoder is interpreting the array of shape-proxys and converting
[:maybe [:set ::sm/keyword]]] ;; them to plain maps. For now we adapt the schema to accept it, but the decoder
;; should be fixed to keep the original proxy objects coming from the plugin.
;; [:vector [:fn shape/shape-proxy?]]
[:vector [:map [:id ::sm/uuid]]]
[:maybe [:set [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]]
:fn (fn [shapes attrs] :fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map :id shapes) attrs))} (apply-token-to-shapes file-id set-id id (map :id shapes) attrs))}
:applyToSelected :applyToSelected
{:schema [:tuple [:maybe [:set ::sm/keyword]]] {:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [attrs] :fn (fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])] (let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))})) (apply-token-to-shapes file-id set-id id selected attrs)))}))

View File

@@ -10,7 +10,6 @@
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class]) (:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class])
#?(:cljs (:require-macros [app.util.object])) #?(:cljs (:require-macros [app.util.object]))
(:require (:require
[app.common.data :as d]
[app.common.json :as json] [app.common.json :as json]
[app.common.schema :as sm] [app.common.schema :as sm]
[clojure.core :as c] [clojure.core :as c]
@@ -157,7 +156,6 @@
this-sym (with-meta (gensym (str rsym "-this-")) {:tag 'js}) this-sym (with-meta (gensym (str rsym "-this-")) {:tag 'js})
target-sym (with-meta (gensym (str rsym "-target-")) {:tag 'js}) target-sym (with-meta (gensym (str rsym "-target-")) {:tag 'js})
cause-sym (gensym "cause-")
make-sym make-sym
(fn [pname prefix] (fn [pname prefix]
@@ -178,7 +176,6 @@
wrap (c/get params :wrap) wrap (c/get params :wrap)
schema-1 (c/get params :schema-1) schema-1 (c/get params :schema-1)
this? (c/get params :this false) this? (c/get params :this false)
on-error (c/get params :on-error)
decode-expr decode-expr
(c/get params :decode/fn) (c/get params :decode/fn)
@@ -217,16 +214,7 @@
(with-meta {:tag 'function})) (with-meta {:tag 'function}))
val-sym val-sym
(gensym (str "val-" (str/slug pname) "-")) (gensym (str "val-" (str/slug pname) "-"))]
wrap-error-handling
(if on-error
(fn [expr]
`(try
~expr
(catch :default ~cause-sym
(~on-error ~cause-sym))))
identity)]
(concat (concat
(when wrap (when wrap
@@ -238,13 +226,8 @@
`(fn [] `(fn []
(let [~this-sym (~'js* "this") (let [~this-sym (~'js* "this")
~fn-sym ~get-expr] ~fn-sym ~get-expr]
~(wrap-error-handling (.call ~fn-sym ~this-sym ~this-sym)))
`(.call ~fn-sym ~this-sym ~this-sym)))) get-expr)])
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
~(wrap-error-handling
`(.call ~fn-sym ~this-sym)))))])
(when set-expr (when set-expr
[schema-sym schema-n [schema-sym schema-n
@@ -258,35 +241,28 @@
(make-sym pname "set-fn") (make-sym pname "set-fn")
`(fn [~val-sym] `(fn [~val-sym]
~(wrap-error-handling (let [~this-sym (~'js* "this")
`(let [~this-sym (~'js* "this") ~fn-sym ~set-expr
~fn-sym ~set-expr
;; We only emit schema and coercer bindings if ;; We only emit schema and coercer bindings if
;; schema-n is provided ;; schema-n is provided
~@(if (some? schema-n) ~@(if (some? schema-n)
[schema-sym [schema-sym `(if (fn? ~schema-sym)
`(if (fn? ~schema-sym) (~schema-sym ~val-sym)
(~schema-sym ~val-sym) ~schema-sym)
~schema-sym)
coercer-sym coercer-sym `(if (nil? ~coercer-sym)
`(if (nil? ~coercer-sym) (sm/coercer ~schema-sym)
(sm/coercer ~schema-sym) ~coercer-sym)
~coercer-sym) val-sym (if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym `(~coercer-sym ~val-sym)]
[])]
val-sym ~(if this?
(if (not= decode-expr 'app.common.json/->clj) `(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(~decode-sym ~val-sym) `(.call ~fn-sym ~this-sym ~val-sym))))])
`(~decode-sym ~val-sym ~decode-options))
val-sym
`(~coercer-sym ~val-sym)]
[])]
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym)))))])
(when fn-expr (when fn-expr
[schema-sym (or schema-n schema-1) [schema-sym (or schema-n schema-1)
@@ -299,12 +275,7 @@
(make-sym pname "get-fn") (make-sym pname "get-fn")
`(fn [] `(fn []
(let [~this-sym (~'js* "this") (let [~this-sym (~'js* "this")
~fn-sym ~(if (and (list? fn-expr) ~fn-sym ~fn-expr
(= 'fn (first fn-expr)))
(let [[sa sb & sother] fn-expr]
`(~sa ~sb ~(wrap-error-handling `(do ~@sother))))
fn-expr)
~fn-sym ~(if this? ~fn-sym ~(if this?
`(.bind ~fn-sym ~this-sym ~this-sym) `(.bind ~fn-sym ~this-sym ~this-sym)
`(.bind ~fn-sym ~this-sym)) `(.bind ~fn-sym ~this-sym))
@@ -313,31 +284,25 @@
;; schema-n or schema-1 is provided ;; schema-n or schema-1 is provided
~@(if (or schema-n schema-1) ~@(if (or schema-n schema-1)
[fn-sym `(fn* [~@(if schema-1 [val-sym] [])] [fn-sym `(fn* [~@(if schema-1 [val-sym] [])]
~(wrap-error-handling (let [~@(if schema-n
`(let [~@(if schema-n [val-sym `(into-array (cljs.core/js-arguments))]
[val-sym `(into-array (cljs.core/js-arguments))] [])
[]) ~val-sym ~(if (not= decode-expr 'app.common.json/->clj)
~val-sym `(~decode-sym ~val-sym)
~(if (not= decode-expr 'app.common.json/->clj) `(~decode-sym ~val-sym ~decode-options))
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~schema-sym ~schema-sym (if (fn? ~schema-sym)
(if (fn? ~schema-sym) (~schema-sym ~val-sym)
(~schema-sym ~val-sym) ~schema-sym)
~schema-sym)
~coercer-sym ~coercer-sym (if (nil? ~coercer-sym)
(if (nil? ~coercer-sym) (sm/coercer ~schema-sym)
(sm/coercer ~schema-sym) ~coercer-sym)
~coercer-sym)
~val-sym ~val-sym (~coercer-sym ~val-sym)]
(~coercer-sym ~val-sym)] ~(if schema-1
`(~fn-sym ~val-sym)
~(if schema-1 `(apply ~fn-sym ~val-sym))))]
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym)))))]
[])] [])]
~(if wrap ~(if wrap
`(~wrap-sym ~fn-sym) `(~wrap-sym ~fn-sym)
@@ -408,19 +373,14 @@
(= :property curr) (= :property curr)
(let [definition (first params)] (let [definition (first params)]
(prn definition (meta definition))
(if (some? definition) (if (some? definition)
(let [definition (if (map? definition) (let [definition (if (map? definition)
(c/merge {:wrap (:wrap tmeta) (c/merge {:wrap (:wrap tmeta)} definition)
:on-error (:on-error tmeta)}
definition)
(-> {:enumerable false} (-> {:enumerable false}
(c/merge (meta definition)) (c/merge (meta definition))
(assoc :wrap (:wrap tmeta)) (assoc :wrap (:wrap tmeta))
(assoc :on-error (:on-error tmeta))
(assoc :fn definition) (assoc :fn definition)
(dissoc :get :set :line :column) (dissoc :get :set)))
(d/without-nils)))
definition (assoc definition :name (name ckey))] definition (assoc definition :name (name ckey))]
(recur (rest params) (recur (rest params)
@@ -465,13 +425,6 @@
(let [o (get o type-symbol)] (let [o (get o type-symbol)]
(= o t)))) (= o t))))
#?(:cljs
(def Proxy
(app.util.object/class
:name "Proxy"
:extends js/Object
:constructor (constantly nil))))
(defmacro reify (defmacro reify
"A domain specific variation of reify that creates anonymous objects "A domain specific variation of reify that creates anonymous objects
on demand with the ability to assign protocol implementations and on demand with the ability to assign protocol implementations and
@@ -489,7 +442,7 @@
obj-sym obj-sym
(gensym "obj-")] (gensym "obj-")]
`(let [~obj-sym (new Proxy) `(let [~obj-sym (cljs.core/js-obj)
~f-sym (fn [] ~type-name)] ~f-sym (fn [] ~type-name)]
(add-properties! ~obj-sym (add-properties! ~obj-sym
{:name ~'js/Symbol.toStringTag {:name ~'js/Symbol.toStringTag

View File

@@ -18,7 +18,7 @@
(let [;; ==== Setup (let [;; ==== Setup
store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
^js context (api/create-context "TEST") ^js context (api/create-context "00000000-0000-0000-0000-000000000000")
_ (set! st/state store) _ (set! st/state store)

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" } assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/contrast-plugin/browser" } assets = { directory = "../../dist/apps/contrast-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/create-palette-plugin" } assets = { directory = "../../dist/apps/create-palette-plugin" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -28,5 +28,5 @@ export default [
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],
rules: {}, rules: {},
}, },
{ ignores: ['vite.config.ts'] }, { ignores: ['vite.config.ts', 'vitest.setup.ts'] },
]; ];

View File

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ describe('Plugins', () => {
it('create grid layout', async () => { it('create grid layout', async () => {
const agent = await Agent(); const agent = await Agent();
const result = await agent.runCode(grid.toString(), { const result = await agent.runCode(grid.toString(), {
screenshot: 'create-gridlayout', screenshot: 'create-gridlayout',
}); });
@@ -83,9 +84,9 @@ describe('Plugins', () => {
it('comments', async () => { it('comments', async () => {
const agent = await Agent(); const agent = await Agent();
console.log(comments.toString());
const result = await agent.runCode(comments.toString(), { const result = await agent.runCode(comments.toString(), {
screenshot: 'create-comments', screenshot: 'create-comments',
avoidSavedStatus: true,
}); });
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });

View File

@@ -1,4 +1,4 @@
import puppeteer from 'puppeteer'; import puppeteer, { ConsoleMessage } from 'puppeteer';
import { PenpotApi } from './api'; import { PenpotApi } from './api';
import { getFileUrl } from './get-file-url'; import { getFileUrl } from './get-file-url';
import { idObjectToArray } from './clean-id'; import { idObjectToArray } from './clean-id';
@@ -56,10 +56,16 @@ export async function Agent() {
console.log('File URL:', fileUrl); console.log('File URL:', fileUrl);
console.log('Launching browser...'); console.log('Launching browser...');
const browser = await puppeteer.launch({}); const browser = await puppeteer.launch({
headless: process.env['E2E_HEADLESS'] !== 'false',
args: ['--ignore-certificate-errors'],
});
const page = await browser.newPage(); const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 }); await page.setViewport({ width: 1920, height: 1080 });
await page.setExtraHTTPHeaders({
'X-Client': 'plugins/e2e:puppeter',
});
console.log('Setting authentication cookie...'); console.log('Setting authentication cookie...');
page.setCookie({ page.setCookie({
@@ -85,8 +91,11 @@ export async function Agent() {
const finish = async () => { const finish = async () => {
console.log('Deleting file and closing browser...'); console.log('Deleting file and closing browser...');
await penpotApi.deleteFile(file['~:id']); // TODO
await browser.close(); // await penpotApi.deleteFile(file['~:id']);
if (process.env['E2E_CLOSE_BROWSER'] !== 'false') {
await browser.close();
}
console.log('Clean up done.'); console.log('Clean up done.');
}; };
@@ -96,11 +105,9 @@ export async function Agent() {
options: { options: {
screenshot?: string; screenshot?: string;
autoFinish?: boolean; autoFinish?: boolean;
avoidSavedStatus?: boolean;
} = { } = {
screenshot: '', screenshot: '',
autoFinish: true, autoFinish: true,
avoidSavedStatus: false,
}, },
) { ) {
const autoFinish = options.autoFinish ?? true; const autoFinish = options.autoFinish ?? true;
@@ -109,28 +116,27 @@ export async function Agent() {
await page.evaluate((testingPlugin) => { await page.evaluate((testingPlugin) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).ɵloadPlugin({ (globalThis as any).ɵloadPlugin({
pluginId: 'TEST', pluginId: '00000000-0000-0000-0000-000000000000',
name: 'Test', name: 'Test',
code: ` code: `
(${testingPlugin})(); (${testingPlugin})();
`, `,
icon: '', icon: '',
description: '', description: '',
permissions: ['content:read', 'content:write'], permissions: [
'content:read',
'content:write',
'library:read',
'library:write',
'user:read',
'comment:read',
'comment:write',
'allow:downloads',
'allow:localstorage',
],
}); });
}, code); }, code);
if (!options.avoidSavedStatus) {
console.log('Waiting for save status...');
await page.waitForSelector(
'.main_ui_workspace_right_header__saved-status',
{
timeout: 10000,
},
);
console.log('Save status found.');
}
if (options.screenshot && screenshotsEnable) { if (options.screenshot && screenshotsEnable) {
console.log('Taking screenshot:', options.screenshot); console.log('Taking screenshot:', options.screenshot);
await page.screenshot({ await page.screenshot({
@@ -138,30 +144,55 @@ export async function Agent() {
}); });
} }
return new Promise((resolve) => { const result = await new Promise((resolve) => {
page.once('console', async (msg) => { const handleConsole = async (msg: ConsoleMessage) => {
const args = (await Promise.all( const args = (await Promise.all(
msg.args().map((arg) => arg.jsonValue()), msg.args().map((arg) => arg.jsonValue()),
)) as Record<string, unknown>[]; )) as unknown[];
const result = Object.values(args[1]) as Shape[]; const type = args[0];
const data = args[1];
if (type !== 'objects' || !data || typeof data !== 'object') {
console.log('Invalid console message, waiting for valid one...');
page.once('console', handleConsole);
return;
}
const result = Object.values(data) as Shape[];
replaceIds(result); replaceIds(result);
console.log('IDs replaced in result.'); console.log('IDs replaced in result.');
resolve(result); resolve(result);
};
if (autoFinish) { page.once('console', handleConsole);
console.log('Auto finish enabled. Cleaning up...');
finish();
}
});
console.log('Evaluating debug.dump_objects...'); console.log('Evaluating debug.dump_objects...');
page.evaluate(` page.evaluate(`
debug.dump_objects(); debug.dump_objects();
`); `);
}); });
await page.waitForNetworkIdle({ idleTime: 2000 });
// Wait for the update-file API call to complete
if (process.env['E2E_WAIT_API_RESPONSE'] === 'true') {
await page.waitForResponse(
(response) =>
response.url().includes('api/main/methods/update-file') &&
response.status() === 200,
{ timeout: 10000 },
);
}
if (autoFinish) {
console.log('Auto finish enabled. Cleaning up...');
await finish();
}
return result;
}, },
finish, finish,
}; };

View File

@@ -1,31 +1,34 @@
import { FileRpc } from '../models/file-rpc.model'; import { FileRpc } from '../models/file-rpc.model';
const apiUrl = 'https://localhost:3449';
const apiUrl = 'http://localhost:3449';
export async function PenpotApi() { export async function PenpotApi() {
if (!process.env['E2E_LOGIN_EMAIL']) { if (!process.env['E2E_LOGIN_EMAIL']) {
throw new Error('E2E_LOGIN_EMAIL not set'); throw new Error('E2E_LOGIN_EMAIL not set');
} }
const body = JSON.stringify({
email: process.env['E2E_LOGIN_EMAIL'],
password: process.env['E2E_LOGIN_PASSWORD'],
});
const resultLoginRequest = await fetch( const resultLoginRequest = await fetch(
`${apiUrl}/api/rpc/command/login-with-password`, `${apiUrl}/api/main/methods/login-with-password`,
{ {
credentials: 'include',
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/transit+json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: body,
'~:email': process.env['E2E_LOGIN_EMAIL'],
'~:password': process.env['E2E_LOGIN_PASSWORD'],
}),
}, },
); );
const loginData = await resultLoginRequest.json(); const loginData = await resultLoginRequest.json();
const authToken = resultLoginRequest.headers const authToken = resultLoginRequest.headers
.get('set-cookie') .getSetCookie()
?.split(';') .find((cookie: string) => cookie.startsWith('auth-token='))
.at(0); ?.split(';')[0];
if (!authToken) { if (!authToken) {
throw new Error('Login failed'); throw new Error('Login failed');
@@ -35,7 +38,7 @@ export async function PenpotApi() {
getAuth: () => authToken, getAuth: () => authToken,
createFile: async () => { createFile: async () => {
const createFileRequest = await fetch( const createFileRequest = await fetch(
`${apiUrl}/api/rpc/command/create-file`, `${apiUrl}/api/main/methods/create-file`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -51,6 +54,9 @@ export async function PenpotApi() {
'fdata/objects-map', 'fdata/objects-map',
'fdata/pointer-map', 'fdata/pointer-map',
'fdata/shape-data-type', 'fdata/shape-data-type',
'fdata/path-data',
'design-tokens/v1',
'variants/v1',
'components/v2', 'components/v2',
'styles/v2', 'styles/v2',
'layout/grid', 'layout/grid',
@@ -61,11 +67,13 @@ export async function PenpotApi() {
}, },
); );
return (await createFileRequest.json()) as FileRpc; const fileData = (await createFileRequest.json()) as FileRpc;
console.log('File data received:', fileData);
return fileData;
}, },
deleteFile: async (fileId: string) => { deleteFile: async (fileId: string) => {
const deleteFileRequest = await fetch( const deleteFileRequest = await fetch(
`${apiUrl}/api/rpc/command/delete-file`, `${apiUrl}/api/main/methods/delete-file`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@@ -6,5 +6,5 @@ export function getFileUrl(file: FileRpc) {
const fileId = cleanId(file['~:id']); const fileId = cleanId(file['~:id']);
const pageId = cleanId(file['~:data']['~:pages'][0]); const pageId = cleanId(file['~:data']['~:pages'][0]);
return `http://localhost:3449/#/workspace/${projectId}/${fileId}?page-id=${pageId}`; return `https://localhost:3449/#/workspace/${projectId}/${fileId}?page-id=${pageId}`;
} }

View File

@@ -7,13 +7,27 @@ export default defineConfig({
testTimeout: 20000, testTimeout: 20000,
watch: false, watch: false,
globals: true, globals: true,
environment: 'happy-dom', environment: 'node',
environmentOptions: {
happyDOM: {
settings: {
disableCSSFileLoading: true,
disableJavaScriptFileLoading: true,
disableJavaScriptEvaluation: true,
enableFileSystemHttpRequests: false,
navigator: {
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
},
},
},
},
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'], reporters: ['default'],
coverage: { coverage: {
reportsDirectory: '../coverage/e2e', reportsDirectory: '../coverage/e2e',
provider: 'v8', provider: 'v8',
}, },
setupFiles: ['dotenv/config'], setupFiles: ['dotenv/config', 'vitest.setup.ts'],
}, },
}); });

View File

@@ -0,0 +1 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/icons-plugin/browser" } assets = { directory = "../../dist/apps/icons-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" } assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -251,7 +251,14 @@ function applyToken(
token.applyToSelected(properties); token.applyToSelected(properties);
} }
// Alternatve way // Alternative way
//
// const selection = penpot.selection;
// if (token && selection) {
// token.applyToShapes(selection, properties);
// }
// Other alternative way
// //
// const selection = penpot.selection; // const selection = penpot.selection;
// if (token && selection) { // if (token && selection) {

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/rename-layers-plugin/browser" } assets = { directory = "../../dist/apps/rename-layers-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "../../dist/apps/table-plugin/browser" } assets = { directory = "../../dist/apps/table-plugin/browser" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -4,12 +4,15 @@
1. **Configure Environment Variables** 1. **Configure Environment Variables**
Create and populate the `.env` file with a valid user mail & password: Create and populate the `apps/e2e/.env` file with a valid user mail & password:
```env ```env
E2E_LOGIN_EMAIL="test@penpot.app" E2E_LOGIN_EMAIL="test@penpot.app"
E2E_LOGIN_PASSWORD="123123123" E2E_LOGIN_PASSWORD="123123123"
E2E_SCREENSHOTS= "true" E2E_SCREENSHOTS="true" # Enable/disable screenshots (default: false)
E2E_HEADLESS="false" # Run browser in headless mode (default: true)
E2E_CLOSE_BROWSER="true" # Close browser after tests (default: true)
E2E_WAIT_API_RESPONSE="false" # Wait for update-file API response (default: false)
``` ```
2. **Run E2E Tests** 2. **Run E2E Tests**
@@ -24,7 +27,7 @@
1. **Adding Tests** 1. **Adding Tests**
Place your test files in the `/apps/e2e/src/**/*.spec.ts` directory. Below is an example of a test file: Place your test files in the `apps/e2e/src/**/*.spec.ts` directory. Below is an example of a test file:
```ts ```ts
import testingPlugin from './plugins/create-board-text-rect'; import testingPlugin from './plugins/create-board-text-rect';
@@ -77,5 +80,5 @@
If you need to refresh all the snapshopts run the test with the update option: If you need to refresh all the snapshopts run the test with the update option:
```bash ```bash
pnpm run test:e2e -- --update pnpm run test:e2e --update
``` ```

View File

@@ -3744,7 +3744,7 @@ export interface ShapeBase extends PluginData {
* and the value set to the attributes will depend on which sets are active * and the value set to the attributes will depend on which sets are active
* (and will change if different sets or themes are activated later). * (and will change if different sets or themes are activated later).
*/ */
readonly tokens: { [property: string]: string }; readonly tokens: { [property in TokenProperty]: string };
/** /**
* @return Returns true if the current shape is inside a component instance * @return Returns true if the current shape is inside a component instance
@@ -5221,7 +5221,7 @@ type TokenDimensionProps =
| 'y' | 'y'
// Stroke width // Stroke width
| 'stroke-width'; | 'strokeWidth';
/** /**
* The properties that a FontFamilies token can be applied to. * The properties that a FontFamilies token can be applied to.

View File

@@ -28,7 +28,9 @@ export const initPluginsRuntime = (contextBuilder: (id: string) => Context) => {
try { try {
console.log('%c[PLUGINS] Initialize runtime', 'color: #008d7c'); console.log('%c[PLUGINS] Initialize runtime', 'color: #008d7c');
setContextBuilder(contextBuilder); setContextBuilder(contextBuilder);
globalThisAny$.ɵcontext = contextBuilder('TEST'); globalThisAny$.ɵcontext = contextBuilder(
'00000000-0000-0000-0000-000000000000',
);
globalThis.ɵloadPlugin = ɵloadPlugin; globalThis.ɵloadPlugin = ɵloadPlugin;
globalThis.ɵloadPluginByUrl = ɵloadPluginByUrl; globalThis.ɵloadPluginByUrl = ɵloadPluginByUrl;
globalThis.ɵunloadPlugin = ɵunloadPlugin; globalThis.ɵunloadPlugin = ɵunloadPlugin;

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "dist/doc" } assets = { directory = "dist/doc" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true

View File

@@ -3,21 +3,6 @@ compatibility_date = "2025-01-01"
assets = { directory = "dist/apps/example-styles" } assets = { directory = "dist/apps/example-styles" }
[observability]
enabled = true
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = false
persist = true
head_sampling_rate = 1
[[routes]] [[routes]]
pattern = "WORKER_URI" pattern = "WORKER_URI"
custom_domain = true custom_domain = true