Compare commits

...
Author SHA1 Message Date
Andrey Antukh 382efe3449 📚 Update changelog 2026-05-12 23:33:39 +02:00
Yamila Moreno 4289cad9ab 🐳 Improve nginx configuration for MCP server (#9565) 2026-05-12 23:28:20 +02:00
Andrey Antukh db7fcfcb1a 🐛 Fix metrics for rpc methods 2026-05-12 19:06:25 +02:00
Andrey Antukh e5c99231da 📚 Update changelog 2026-05-12 18:43:08 +02:00
Yamila Moreno 02c3d2c27c 🐳 Add mcp server to release workflow 2026-05-12 18:38:17 +02:00
Yamila Moreno eb22c59e5a 🐳 Add penpot-mcp service to official docker-compose.yml 2026-05-12 18:31:13 +02:00
Andrey Antukh 947f6d392d 🎉 Add chunked upload support for font variants (#9551)
*  Add additional logging and validation for image upload

* 🎉 Add chunked upload support for font variants

Extend the font variant upload flow across frontend, backend, and common
to support the standardized chunked upload protocol.

**Backend:**
- Add \`:font-max-file-size\` config default (30 MiB) and schema entry
- Add \`validate-font-size!\` in \`media.clj\` (mirrors
  \`validate-media-size!\`, raises \`:font-max-file-size-reached\`)
- Extend \`schema:create-font-variant\` to accept either \`:data\`
  (legacy bytes or chunk-vector) or \`:uploads\` (new chunked session
  map), with a validator requiring exactly one
- Add \`prepare-font-data-from-uploads\`: assembles each chunked
  session via \`cmedia/assemble-chunks\`, validates type+size
- Add \`prepare-font-data-from-legacy\`: normalises legacy byte/chunk
  entries, writing to a tempfile (joining via SequenceInputStream),
  validates type+size
- Add structured logging ("init"/"end") with \`:size\`, \`:mtypes\`,
  and \`:elapsed\` in \`create-font-variant\`

**Frontend:**
- \`upload-blob-chunked\` accepts a per-caller \`:chunk-size\` option
- Add \`font-upload-chunk-size\` (10 MiB) and \`upload-font-variant\`
  fn that uploads each mtype as a separate chunked session
- \`on-upload*\` in dashboard fonts now calls \`upload-font-variant\`
  instead of issuing \`create-font-variant\` RPC directly
- \`process-upload\` stores raw ArrayBuffer instead of chunking
  client-side

**Common:**
- Replace \`"font/opentype"\` with \`"font/woff2"\` in \`font-types\`

**Tests:**
- 25 tests / 224 assertions covering all three upload paths (direct
  bytes, legacy chunk-vector, new chunked sessions), size validation,
  and media type validation

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add a script for check the commit format locally

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-12 18:30:19 +02:00
Andrey Antukh ade0d2d0a8 📎 Update changelog with PR info 2026-05-12 13:01:42 +02:00
Yamila Moreno 328efd4e16 📚 Add notice regarding architectural constraints with MCP Server (#9423) 2026-05-12 10:06:13 +02:00
Andrey Antukh 11a72abdcd 📎 Update version on mcp server 2026-05-12 10:04:12 +02:00
Andrey Antukh bd3ca6f8e5 📚 Update changelog 2026-05-12 09:33:33 +02:00
Andrey Antukh 843a4a5b58 🐛 Fix mattermost and database logger related to the audit event change 2026-05-11 17:08:38 +02:00
Andrey Antukh 102c97040a 🐛 Fix unexpected exception on handling webhook events 2026-05-11 16:23:14 +02:00
Andrey Antukh 8f4f948104 🐛 Skip the ssrf check on internal audit-log archive task 2026-05-11 15:22:59 +02:00
Dr. Dominik Jain 1f9f4126b7 Improve MCP server logging, adding Loki support (#9425)
*  Improve MCP server logging

Log only fingerprints of user tokens

*  Add Loki transport support to MCP server logger

Loki logging is enabled iff PENPOT_LOGGERS_LOKI_URI is non-empty.

File logging is now enabled iff PENPOT_MCP_LOG_DIR is set to a non-empty value
(previously defaulted to the "logs" directory when unset).

GitHub #9415
2026-05-11 14:01:05 +02:00
Andrey Antukh feb49bc07a 🐛 Add missing migrations for audit-log tables 2026-05-11 13:28:53 +02:00
Andrey Antukh 7d4be33d4f 🎉 Add telemetry anonymous event collection (#9483)
* 🎉 Add telemetry anonymous event collection

Rewrite the audit logging subsystem to support three operating modes and
add anonymous telemetry event collection:

Modes:
- A (audit-log only): events persisted with full context
- B (audit-log + telemetry): same as A, plus events are collected for
  telemetry shipping
- C (telemetry-only): events stored anonymously with PII stripped,
  telemetry flag active, audit-log flag inactive

Audit system refactoring (app.loggers.audit):
- Replace qualified map keys (::audit/name etc.) with plain keywords
- Rename submit! -> submit, insert! -> insert, prepare-event ->
  prepare-rpc-event
- Add submit* as a lower-level public API
- Add process-event dispatch function that handles all three modes and
  webhooks in a single tx-run!
- Add :id to event schema (auto-generated if omitted)
- Add filter-telemetry-props: anonymises event props per event type.
  Keeps UUID/boolean/number values; for login/identify events preserves
  lang, auth-backend, email-domain; for navigate events preserves route,
  file-id, team-id, page-id; instance-start trigger passes through.
- Add filter-telemetry-context: retains only safe context keys.
  Backend: version, initiator, client-version, client-user-agent.
  Frontend: browser, os, locale, screen metrics, event-origin.
- Timestamps truncated to day precision via ct/truncate for telemetry
  storage
- PII stripped: props emptied, ip-addr zeroed, session-linking and
  access-token fields removed from context

Config (app.config):
- Derive :enable-telemetry flag from telemetry-enabled config option

Email utilities (app.email):
- Add email/clean and email/get-domain helper functions for domain
  extraction from email addresses

Setup (app.setup):
- Emit instance-start trigger event at system startup
- Simplify handle-instance-id (remove read-only check)

RPC layer (app.rpc):
- wrap-audit now activates when :telemetry flag is set
- Add :request-id to RPC params context for event correlation

RPC commands (management, teams_invitations, verify_token, OIDC auth,
webhooks): migrate all audit call sites to use the new plain-key API

SREPL (app.srepl.main):
- Migrate all audit/insert! calls to audit/insert with plain keys

Telemetry task (app.tasks.telemetry):
- Restructure legacy report into make-legacy-request; distinguish
  payload type as :telemetry-legacy-report
- Add collect-and-send-audit-events: loop fetching up to 10,000 rows
  per iteration, encodes and sends each page, deletes on success,
  stops immediately on failure for retry
- Add send-event-batch: POSTs fressian+zstd batch (base64 via
  blob/encode-str) to the telemetry endpoint with instance-id per event
- Add gc-telemetry-events: enforces 100,000-row safety cap by dropping
  oldest rows first
- Add delete-sent-events: deletes successfully shipped rows by id

Blob utilities (app.util.blob):
- Add encode-str/decode-str: combine fressian+zstd encoding with URL-
  safe base64 for JSON-safe string transport

Database:
- Add migration 0145: index on audit_log (source, created_at ASC) for
  efficient telemetry batch collection queries

Frontend:
- Always initialize event system regardless of :audit-log flag
- Defer auth events (signin identify) to after profile is set
- Refactor event subsystem for telemetry support

Tests (21 test vars, 94 assertions in tasks-telemetry-test):
- Cover all code paths: disabled/enabled telemetry, no-events no-op,
  happy-path batch send and delete, failure retention, payload anonymity,
  context stripping, timestamp day precision, batch encoding round-trip,
  multi-page iteration, GC cap enforcement, partial failure handling
- blob encode-str/decode-str round-trip tests (14 test vars)
- RPC audit integration tests (5 test vars)

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add pr feedback changes

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 12:42:01 +02:00
Andrey Antukh cd882f9ebd 🔧 Add minor changes to devenv config 2026-05-11 12:39:03 +02:00
Andrey Antukh b312e6b059 📚 Update changelog 2026-05-11 11:26:54 +02:00
Andrey Antukh 15379f37f5 🐛 Fix maximum call stack size exceeded in SSE read-stream (#9484)
The recursive `read-items` function in `app.util.sse/read-stream`
caused a synchronous stack overflow when reading buffered stream
data. Each `rx/mapcat` call chained another recursive invocation
on the same call stack without yielding to the event loop.

Replace the recursive pattern with an `rx/create`-based async pump
that uses Promise `.then()` chaining, keeping the call stack depth
constant regardless of stream size.

Also add progress reporting with names and IDs during binfile
export and import, and bump `eventsource-parser` dependency.

Closes #9470

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 11:06:15 +02:00
Andrey Antukh 1a212a2769 Merge remote-tracking branch 'origin/main-staging' 2026-05-11 08:46:25 +02:00
Alonso Torres 9f05ba2fdf Add plugins and mcp event data (#9228)
*  Add plugins and mcp event data

* ♻️ Changed data-event ::ev/event to ev/event
2026-05-11 08:36:53 +02:00
andrés gonzález 9c771ae6b9 🐛 Fix MCP integrations copy button to match displayed URL (#9239) 2026-05-10 19:23:03 +02:00
Andrey Antukh 9b336e9a3d Add nrepl-eval script and skill 2026-05-10 10:49:53 +02:00
Andrey Antukh cf3455a487 📎 Add missing entry on CHANGES.md 2026-05-10 09:18:52 +02:00
Francis Santiago e9588f3939 🐳 Reuse shared Nginx security headers (#9473)
Signed-off-by: Francis Santiago <francis.santiago@kaleidos.net>
2026-05-08 14:11:09 +02:00
Andrey Antukh a50785f105 📎 Update changelog 2026-05-08 09:29:28 +02:00
Andrey Antukh 279231240d 🐛 Harden outbound HTTP requests against SSRF and restrict assets handlers (#9390)
* ⬆️ Update root deps

* 🐛 Harden outbound HTTP requests against SSRF and restrict unauthenticated asset access

- Add app.util.ssrf URL/host validator that resolves hostnames and blocks
  loopback, link-local, site-local, cloud metadata, and operator-supplied CIDRs
- Add app.media.sanitize image EOF truncator that strips trailing data after
  PNG IEND, JPEG EOI, GIF trailer, and WebP RIFF markers
- Disable HTTP client auto-redirect; add req-with-redirects! helper that
  revalidates every redirect hop against the SSRF blocklist
- Wire SSRF validation and EOF sanitization into media/download-image
- Validate webhook URLs and OIDC profile picture URLs against SSRF
- Restrict /assets/by-id to require authentication for non-public buckets
  (profile) while keeping public access for file-media-object,
  file-object-thumbnail, team-font-variant, and file-data-fragment
- Add config knobs: ssrf-protection-enabled, ssrf-allowed-hosts,
  ssrf-extra-blocked-cidrs

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 09:18:22 +02:00
Andrey Antukh 3496435e69 📚 Update changelog 2026-05-08 00:32:21 +02:00
Andrey Antukh d103feebfa 📚 Update changelog 2026-05-07 23:57:49 +02:00
Dr. Dominik Jain 362440fead 🚑 Use base64 envelope for Uint8Array task results to avoid JSON expansion (#9431)
Resolves #9420 (critical memory usage issue in PROD deployment)

When the plugin's ExecuteCodeTaskHandler returns a Uint8Array (e.g. from penpotUtils.exportImage),
JSON.stringify previously serialized it as an object with numeric string keys,
causing ~10x payload expansion and large peak heap usage on the server side.

The plugin now wraps a top-level Uint8Array result in a tagged envelope
{ __type: "base64", data: <base64> }, and ImageContent.byteData decodes this envelope
on the server. The legacy numeric-keyed-object path is retained as a fallback for
compatibility with older plugin builds.
2026-05-07 23:51:50 +02:00
Dr. Dominik Jain 6a44b19311 🐛 Fix keep-alive interval leak in PluginBridge (#9435)
The ping interval was stored in a single variable shared across all
WebSocket connections, so each new connection overwrote the previous
handle and leaked the prior interval.

Move the interval onto ClientConnection as a per-connection field,
and centralize teardown in a new removeConnection(ws) method used
by the close, error and duplicate token rejection paths.

Resolves #9430
2026-05-07 20:37:22 +02:00
Andrey Antukh 798ee46b4a 🐛 Bind MCP ReplServer to localhost to prevent unauthenticated RCE
The ReplServer Express app was calling `app.listen(port)` with no host
argument, causing Node/Express to default to binding on all interfaces
(0.0.0.0). Combined with the unauthenticated /execute endpoint, any
network peer could POST arbitrary JS and get it run inside the MCP
process.

Fix: add a `host` parameter (default "localhost") to the ReplServer
constructor and pass it to `app.listen`. The call site in
PenpotMcpServer now forwards `this.host` (sourced from
PENPOT_MCP_SERVER_HOST env var, default "localhost"), so environment-
variable overrides continue to work.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-07 12:59:31 +02:00
Andrey Antukh 697a825d76 📚 Update opencode planner agent 2026-05-07 01:04:38 +02:00
Dexterity db77780227 🐛 Fix MCP "active in another tab" notification not clearing (#9321) 2026-05-06 17:42:34 +02:00
Dexterity ae7c7a7972 🐛 Fix swapped analytics event names on MCP tab-switch dialog (#9322) 2026-05-06 17:41:16 +02:00
Andrey Antukh 54928e9ffb Merge branch 'backport-2.14' 2026-05-06 14:38:17 +02:00
Andrey Antukh df01f76056 🐛 Fix incorrect invitation token handling on register process (#9380)
* 🐛 Fix incorrect invitation token handling on register process

- Reject prepare-register-profile when an active profile already
  exists for the requested email.
- Stop embedding an existing profile's :profile-id into the
  prepared-register JWE. Profile resolution in register-profile is
  now done exclusively by email lookup, never by a JWE claim.
- Add created? guard to the invitation-success branch in
  register-profile, so existing profiles (active or not) cannot
  reach session creation via anonymous registration.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Restructure invitation handling inside register-profile

Move the invitation-success branch into the created? sub-cond so it
sits alongside the other post-creation branches, making the control
flow consistent.

- Active new profile + matching invitation: mint session and return
  :invitation-token (frontend redirects to :auth-verify-token).
- Not-yet-active new profile + matching invitation: embed the
  invitation token inside the verify-email JWE and send the
  verification email. When the user clicks the link, they get
  logged in and the frontend completes the team-invitation flow.
- Extend send-email-verification! with an optional invitation-token
  parameter propagated into the verify-email JWE claims.
- Update the frontend verify-email handler to navigate to
  :auth-verify-token when the response carries :invitation-token.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Handle email-already-exists error on registration form

Add a specific handler for the [:validation :email-already-exists] error
code in the registration form's on-error callback. The backend raises
this error when an active profile already exists for the requested email,
but the frontend was falling through to the generic error message.

Now it shows the existing "Email already used" i18n message instead of
the generic "Something wrong has happened" toast.

* 🐛 Reset submitted state on registration form error

The on-error handler in the registration form was not resetting the
submitted? state, causing the submit button to remain disabled after
any error. The completion callback in rx/subs! only fires on success,
not on error.

Add (reset! submitted? false) at the beginning of the on-error handler
so the form becomes submittable again after any error, allowing the user
to fix their input and retry.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 14:32:31 +02:00
Andrey Antukh 1e1ca82ba5 📚 Add missing changelog entry and document changelog locations
Add changelog entry for the fix-incorrect-invitation-token-handling
change (PR #9380) under `## 2.15.0 (Unreleased)` > `:bug: Bugs fixed`.
Add a `## Changelogs` section to AGENTS.md documenting both changelog
locations (main project: `CHANGES.md`, plugins: `plugins/CHANGELOG.md`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 12:24:55 +00:00
Andrey Antukh 9e681260cc 🐛 Fix incorrect invitation token handling on register process (#9380)
* 🐛 Fix incorrect invitation token handling on register process

- Reject prepare-register-profile when an active profile already
  exists for the requested email.
- Stop embedding an existing profile's :profile-id into the
  prepared-register JWE. Profile resolution in register-profile is
  now done exclusively by email lookup, never by a JWE claim.
- Add created? guard to the invitation-success branch in
  register-profile, so existing profiles (active or not) cannot
  reach session creation via anonymous registration.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Restructure invitation handling inside register-profile

Move the invitation-success branch into the created? sub-cond so it
sits alongside the other post-creation branches, making the control
flow consistent.

- Active new profile + matching invitation: mint session and return
  :invitation-token (frontend redirects to :auth-verify-token).
- Not-yet-active new profile + matching invitation: embed the
  invitation token inside the verify-email JWE and send the
  verification email. When the user clicks the link, they get
  logged in and the frontend completes the team-invitation flow.
- Extend send-email-verification! with an optional invitation-token
  parameter propagated into the verify-email JWE claims.
- Update the frontend verify-email handler to navigate to
  :auth-verify-token when the response carries :invitation-token.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Handle email-already-exists error on registration form

Add a specific handler for the [:validation :email-already-exists] error
code in the registration form's on-error callback. The backend raises
this error when an active profile already exists for the requested email,
but the frontend was falling through to the generic error message.

Now it shows the existing "Email already used" i18n message instead of
the generic "Something wrong has happened" toast.

* 🐛 Reset submitted state on registration form error

The on-error handler in the registration form was not resetting the
submitted? state, causing the submit button to remain disabled after
any error. The completion callback in rx/subs! only fires on success,
not on error.

Add (reset! submitted? false) at the beginning of the on-error handler
so the form becomes submittable again after any error, allowing the user
to fix their input and retry.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 14:20:55 +02:00
Eva Marco ce24fed32b 🐛 Fix incorrect text-edition warning when applying tokens (#9355) 2026-05-06 08:47:30 +02:00
andrés gonzález 66337f2ab9 📚 Add WebGL Troubleshooting Guide 2026-05-04 09:34:19 +02:00
Alonso Torres c9b81284d2 🐛 Fix z-index for profile menu (#9257) 2026-04-30 11:28:27 +02:00
Andrey Antukh 1de8a074ef 🐛 Fix incorrect gh client install on devenv 2026-04-30 11:23:44 +02:00
Andrey Antukh e81dad21ea 🎉 Add backport-commit skill for manual diff-based commit porting
Introduce a new OpenCode workflow skill that guides users through
backporting commits by applying diffs instead of using cherry-pick.
This is useful when cherry-pick is undesirable (e.g. divergent
histories, binary conflicts, or partial porting).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 19:09:25 +02:00
Andrey Antukh d06b45ec90 🐛 Fix Plugin API token application for JS array of strings
Two coupled defects made shape.applyToken(), token.applyToShapes() and

token.applyToSelected() silently no-op when invoked from JavaScript with

an array of strings (e.g. token.applyToShapes([rect], ["fill"])):

1. token-attr-plugin->token-attr only consulted its alias map when the

   input was already a keyword; string inputs fell through unchanged,

   causing downstream token-attr? to return false.

2. The inner schemas used plain [:set ...] which lacks the :decode/json

   transformer for JS array -> Clojure set coercion. Switching to

   Penpot's custom [::sm/set ...] lets the standard JSON decoder

   pipeline handle the conversion automatically.

This is a backport of commit 1eac3e2be5

which fixes GitHub #9162.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 19:09:25 +02:00
Andrey Antukh 1e09e00634 Encourage use of layouts and proper naming in MCP
Improve MCP instructions on design creation:
 * Agents should make use of layouts when appropriate
 * Agents should name all elements appropriately

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 16:12:50 +00:00
Andrey Antukh 4829b843b2 🐛 Fix dashboard modal clipping behind sidebar (#9233)
Backport from develop commit 510a015424.

- Fix release notes modal appearing behind the dashboard sidebar (by @RenzoMXD)
- Change sidebar z-index from dropdown to panels layer

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 15:52:48 +00:00
Yamila Moreno d627d1cfac Improve team name validation (#9176) 2026-04-29 08:59:09 +02:00
Andrey Antukh 94827f1848 📎 Update versionon mcp/package.json 2026-04-28 12:38:44 +02:00
Andrey Antukh 42c9c4a929 Merge remote-tracking branch 'origin/main-staging' 2026-04-28 12:37:02 +02:00
Andrey Antukh ccd1da40ca 📎 Update mcp package.json version 2026-04-28 10:26:23 +02:00
Eva Marco c269df1441 🐛 Fix empty warning on login (#9056) 2026-04-28 10:25:33 +02:00
Juan de la Cruz 40ee1960a1 📚 Add 2.15.0 onboarding slides (#9172)
* 🎉 Add new slides content

* 🎉 Add new slides imgs

* 🐛 Fix a typo
2026-04-28 10:21:56 +02:00
Andrey Antukh c0989d4261 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-28 10:08:51 +02:00
andrés gonzález aabdb69218 📚 Update MCP docs for public release (#9184) 2026-04-28 09:37:40 +02:00
Luis de Dios a35b61ee0c 🐛 Fix put onboarding modals of top of libraries & templates panel (#9178) 2026-04-28 09:26:15 +02:00
Andrey Antukh 4e1968bbab 📎 Add updated version of github cli to devenv 2026-04-28 00:01:46 +02:00
Andrey Antukh 592cc47336 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-27 20:36:21 +02:00
Andrey Antukh a58dbec8f2 ⬆️ Update root repo deps 2026-04-27 20:35:46 +02:00
Andrey Antukh df4ffb9147 📚 Update prompt-assistant agent file 2026-04-27 20:35:28 +02:00
Andrey Antukh ac5736957e 📚 Update commiter opencode agent 2026-04-27 20:34:45 +02:00
Andrey Antukh eba4f15bba Backport transit and plugins hardening compatibility issue
From staging
2026-04-27 19:04:43 +02:00
Andrey Antukh 839754715a 📚 Update changelog 2026-04-27 17:30:02 +02:00
Andrey Antukh a3b9d7bed7 📎 Fix fmt issue 2026-04-27 17:26:59 +02:00
Andrey Antukh 57f1b80013 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-27 17:26:30 +02:00
Andrey Antukh cbd5f7795b Add minor compatibility adjustments for audit archive task (#8491) 2026-04-27 16:15:35 +02:00
Luis de Dios edccda2038 🐛 Fix remove prints 2026-04-27 15:26:55 +02:00
Andrey Antukh feec89679a Merge remote-tracking branch 'origin/main' into main-staging 2026-04-27 09:58:13 +02:00
Luis de Dios a5a8ab5de6 🐛 Fix MCP status is displayed as disabled when setting MCP key without expiration date
Fixes #14058 and #14061 in Taiga
2026-04-27 09:37:11 +02:00
Andrey Antukh 37cba3355d 🔧 Update opencode tooling, agents, and devenv
Update agent configurations: change commiter mode to all, rename
engineer agent to "Penpot Engineer", and remove obsolete testing agent.

Add new read-only planner agent for architecture analysis and planning.

Add four new skills: bat-cat (syntax-highlighted cat clone), fd-find
(fast file finder), jq-json-processor (JSON processor), and ripgrep
(fast text search).

Add fd-find and bat packages to devenv Dockerfile.

Update .gitignore to exclude opencode package-lock and plans directory.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-27 00:25:23 +02:00
Andrey Antukh 6d9019c383 📚 Improve pull request documentation in CONTRIBUTING.md
Expand the Pull Requests section with detailed guidance on PR title
format, description expectations, branch naming conventions, the review
process, and a list of PRs that will not be accepted. Also clarify the
'Discuss Before Building' rule to link to GitHub Issues and Discussions
and reference Taiga stories. Update the Table of Contents with nested
links for all new subsections.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-25 13:22:17 +00:00
Yamila Moreno 7031052c4e 🐛 Prevent invitations to blacklisted domains 2026-04-24 16:48:59 +02:00
Andrey Antukh 0b6416e53b Merge remote-tracking branch 'origin/main' into main-staging 2026-04-24 14:09:03 +02:00
Andrey Antukh d380efdb0c ⬆️ Update devenv dependencies (#9142)
* ⬆️ Update devenv dependencies

*  Fix formatting issues

* 📎 Fix linter issues
2026-04-24 14:07:51 +02:00
Andrey Antukh 29ba336928 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-24 11:58:50 +02:00
Andrey Antukh cfb076dd61 📚 Update AGENTS.md with common github operations 2026-04-24 11:45:36 +02:00
Eva MarcoandAndrey Antukh 5a7ba7ee7e 🐛 Fix multiple selection on shapes with token applied to stroke-color (#9110)
*  Remove the need to navigate to page for deletion operation

* 🐛 Fix multiple selection with applied-tokens on stroke-color

* 🐛 Fix button position on page header

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-24 09:47:44 +02:00
Andrey Antukh 20c6a28b52 📎 Add commit agent for opencode 2026-04-24 08:54:01 +02:00
Andrey Antukh fd38f5b431 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-24 08:18:55 +02:00
Andrey Antukh 2d5e50f352 ⬆️ Update root repo deps 2026-04-24 08:17:32 +02:00
Luis de Dios cd417443f6 🐛 Fix layer hierarchy to match old and new SCSS (#9126) 2026-04-23 18:00:40 +02:00
Andrey Antukh c6b6b9ce00 📎 Update changelog 2026-04-23 09:59:11 +02:00
Yamila Moreno 5f7de04efe 🚑 Fix email blacklisting (#9122) 2026-04-23 09:42:40 +02:00
Yamila Moreno dc8073f924 🐳 Add PENPOT_PUBLIC_URI to penpot-frontend 2026-04-23 09:06:10 +02:00
Andrey Antukh ba42cc04b7 ♻️ Derive v-sizing from values instead of passing as prop
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-22 14:17:15 +00:00
Luis de Dios b60695f54a 🐛 Fix indicate that the mcp is disabled if the mcp key has expired
If the mcp key has expired, the switch that indicates the status in the dashboard will appear as disabled, and will show a modal for regenerate the key. It will also appear as disabled in the workspace, not allowing the plugin to connect
2026-04-22 16:00:52 +02:00
Yamila Moreno 3c542a1abc 🐛 Fix email validation (#9037) 2026-04-22 15:59:28 +02:00
Andrey Antukh b0b2c0d264 📎 Update version on mcp/ module 2026-04-22 13:18:24 +02:00
Andrey Antukh 88008ce16c 📎 Update mcp types yaml file 2026-04-22 13:11:10 +02:00
Andrey Antukh 75d99a0725 🔧 Add missing public uri handling on nginx entrypoint 2026-04-22 13:11:10 +02:00
Andrey Antukh 09637f9794 Allow render entrypoint load alternative config
The render entrypoint is used by exporter
2026-04-22 13:11:10 +02:00
Andrey Antukh 3225319e0c 🐛 Fix frontend tests 2026-04-22 12:54:07 +02:00
Yamila Moreno 6de5370a0b 🐛 Fix nginx configuration for mcp 2026-04-22 10:29:52 +02:00
Andrey Antukh 448b5d4786 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-22 09:58:20 +02:00
Andrey Antukh 47b3667248 🐛 Fix exporter renderer URI path construction
Apply consistent path construction across bitmap, PDF, and SVG
renderers in the exporter. Use path join utilities instead of
hardcoding the render.html path, ensuring the path is properly
appended to the public URI base path.

- bitmap.cljs: Use u/ensure-path-slash and u/join for path
- pdf.cljs: Use u/join and ensure-path-slash on base-uri
- svg.cljs: Use u/ensure-path-slash and u/join for path
2026-04-22 09:52:44 +02:00
Andrey Antukh 98e8160875 ♻️ Remove worker URI from global templates and compute from public URI
- Remove penpotWorkerURI from index.mustache and rasterizer.mustache templates
- Remove worker_main entry from the build manifest
- Construct worker URI in config.cljs by joining public-uri with worker path
- Fix global variable casing for plugins-list-uri and templates-uri
- Fix alignment in worker.cljs let bindings
2026-04-22 09:52:44 +02:00
Yamila Moreno 6ea7a64e01 Add nginx configuration for mcp server 2026-04-22 09:33:58 +02:00
Andrey Antukh 97d234a566 Add 2h min-age threshold to storage/gc_touched task
Skip storage objects touched less than 2 hours ago, matching the pattern
used by upload-session-gc. Update all affected tests to advance the clock
past the threshold using ct/*clock* bindings.
2026-04-22 08:48:04 +02:00
Andrey Antukh f716995ffd 📚 Update changelog 2026-04-21 21:08:57 +02:00
Andrey Antukh e5f9c1e863 🎉 Add chunked upload API for large media and binary files
Introduce a purpose-agnostic three-step session-based upload API that
allows uploading large binary blobs (media files and .penpot imports)
without hitting multipart size limits.

Backend:
- Migration 0147: new `upload_session` table (profile_id, total_chunks,
  created_at) with indexes on profile_id and created_at.
- Three new RPC commands in media.clj:
    * `create-upload-session`  – allocates a session row; enforces
      `upload-sessions-per-profile` and `upload-chunks-per-session`
      quota limits (configurable in config.clj, defaults 5 / 20).
    * `upload-chunk`           – stores each slice as a storage object;
      validates chunk index bounds and profile ownership.
    * `assemble-file-media-object` – reassembles chunks via the shared
      `assemble-chunks!` helper and creates the final media object.
- `assemble-chunks!` is a public helper in media.clj shared by both
  `assemble-file-media-object` and `import-binfile`.
- `import-binfile` (binfile.clj): accepts an optional `upload-id` param;
  when provided, materialises the temp file from chunks instead of
  expecting an inline multipart body, removing the 200 MiB body limit
  on .penpot imports.  Schema updated with an `:and` validator requiring
  either `:file` or `:upload-id`.
- quotes.clj: new `upload-sessions-per-profile` quota check.
- Background GC task (`tasks/upload_session_gc.clj`): deletes stalled
  (never-completed) sessions older than 1 hour; scheduled daily at
  midnight via the cron system in main.clj.
- backend/AGENTS.md: document the background-task wiring pattern.

Frontend:
- New `app.main.data.uploads` namespace: generic `upload-blob-chunked`
  helper drives steps 1–2 (create session + upload all chunks with a
  concurrency cap of 2) and emits `{:session-id uuid}` for callers.
- `config.cljs`: expose `upload-chunk-size` (default 25 MiB, overridable
  via `penpotUploadChunkSize` global).
- `workspace/media.cljs`: blobs ≥ chunk-size go through the chunked path
  (`upload-blob-chunked` → `assemble-file-media-object`); smaller blobs
  use the existing direct `upload-file-media-object` path.
  `handle-media-error` simplified; `on-error` callback removed.
- `worker/import.cljs`: new `import-blob-via-upload` helper replaces the
  inline multipart approach for both binfile-v1 and binfile-v3 imports.
- `repo.cljs`: `:upload-chunk` derived as a `::multipart-upload`;
  `form-data?` removed from `import-binfile` (JSON params only).

Tests:
- Backend (rpc_media_test.clj): happy path, idempotency, permission
  isolation, invalid media type, missing chunks, session-not-found,
  chunk-index out-of-range, and quota-limit scenarios.
- Frontend (uploads_test.cljs): session creation and chunk-count
  correctness for `upload-blob-chunked`.
- Frontend (workspace_media_test.cljs): direct-upload path for small
  blobs, chunked path for large blobs, and chunk-count correctness for
  `process-blobs`.
- `helpers/http.cljs`: shared fetch-mock helpers (`install-fetch-mock!`,
  `make-json-response`, `make-transit-response`, `url->cmd`).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 18:51:10 +00:00
Andrey Antukh a395768987 🐛 Fix incorrect handlig of version restore operation (#9041)
- Add session ID tracking to RPC layer (backend and frontend)
- Send session ID header with RPC requests for request correlation
- Rename file-restore to file-restored for consistency
- Extract initialize-file function from initialize-workspace flow
- Improve file restoration initialization with wait-for-persistence
- Extract initialize-version event handler for version restoration
- Fix viewport key generation with file version numbers for proper re-renders
- Update layout item schema and constraints to use internal sizing state
- Add v-sizing state retrieval in layout-size-constraints component
- Refactor file-change notifications stream handling with rx/map
- Fix team-id lookup in restore-version-from-plugins

Improves request traceability across frontend/backend sessions and streamlines
the workspace initialization flow for file restoration scenarios.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 20:50:41 +02:00
Andrey Antukh 8f2c467b82 Merge remote-tracking branch 'origin/main' into main-staging 2026-04-21 20:44:31 +02:00
Andrey Antukh aed2f8a8f8 🐛 Fix removeChild errors from unmount race conditions (#8927)
Guard imperative DOM operations (removeChild, RAF callbacks) against
race conditions where React has already unmounted the target nodes.

- assets/common.cljs: add dom/child? guard before removeChild in RAF
- dynamic_modifiers.cljs: capture RAF IDs and cancel them on cleanup;
  add null guards for DOM nodes that may no longer exist
- hooks.cljs: guard portal container removal with dom/child? check
- errors.cljs: extract is-ignorable-exception? to a top-level defn
  and add NotFoundError/removeChild to ignorable exceptions, since
  these are caused by browser extensions modifying React-managed DOM
- Add unit tests for is-ignorable-exception? predicate

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-21 17:31:05 +02:00
Yamila Moreno f19c968bc6 🔧 Add main-staging workflow 2026-04-21 15:40:51 +02:00
Yamila Moreno d5cf7dcf9d 🔧 Add main-staging workflow 2026-04-21 15:40:37 +02:00
Andrey Antukh eeeb698d91 ⬆️ Bump opencode-ai dev dependency 1.4.3 -> 1.14.19
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-20 20:13:15 +02:00
Andrey Antukh 3a39676969 Backport MCP from staging (part 1) 2026-04-20 19:37:02 +02:00
Yamila Moreno b38912f3cb 🔧 Add short tag to DocherHub release (#8864) 2026-04-16 18:22:22 +02:00
Andrey Antukh 69e505a6a2 📎 Update changelog 2026-04-16 10:21:15 +02:00
Andrey Antukh 390796f36e 📎 Update changelog 2026-04-16 10:20:05 +02:00
Andrey Antukh de27ea904d Add minor adjustments to the auth events (#9027) 2026-04-16 09:59:45 +02:00
Andrey Antukh f5271dabee 🐛 Fix error handling issues (#8962)
* 🚑 Fix RangeError from re-entrant error handling in errors.cljs

Two complementary changes to prevent 'RangeError: Maximum call stack
size exceeded' when an error fires while the potok store error pipeline
is still on the call stack:

1. Re-entrancy guard on on-error: a volatile flag (handling-error?)
   is set true for the duration of each on-error invocation. Any
   nested call (e.g. from a notification emit that itself throws) is
   suppressed with a console.error instead of recursing indefinitely.

2. Async notification in flash: the st/emit!(ntf/show ...) call is
   now wrapped in ts/schedule (setTimeout 0) so the notification event
   is pushed to the store on the next event-loop tick, outside the
   error-handler call stack. This matches the pattern already used by
   the :worker-error, :svg-parser and :comment-error handlers.

* 🐛 Add unit tests for app.main.errors

Test coverage for the error-handling module:

- stale-asset-error?: 6 cases covering keyword-constant and
  protocol-dispatch mismatch signatures, plus negative cases
- exception->error-data: plain JS Error, ex-info with/without :hint
- on-error dispatch: map errors routed via ptk/handle-error, JS
  exceptions wrapped into error-data before dispatch
- Re-entrancy guard: verifies that a second on-error call issued
  from within a handle-error method is suppressed (exactly one
  handler invocation)

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-15 23:37:04 +02:00
Elena Torró 9cd1542dd9 Merge pull request #9000 from penpot/niwinz-main-bugfixes
🐛 Add several fixes for app.common.types namespace
2026-04-15 11:51:14 +02:00
Andrey Antukh 2e97f01838 🐛 Fix safe-subvec 2-arity rejecting start=0
The guard used (> start 0) instead of (>= start 0), so
(safe-subvec v 0) returned nil instead of the full vector.
2026-04-15 11:42:49 +02:00
Andrey Antukh 176edadb6f 🐛 Fix nan? returning false for ##NaN on JVM
Clojure's = uses .equals on doubles, and Double.equals(Double.NaN)
returns true, so (not= v v) was always false for NaN. Use
Double/isNaN with a number? guard instead.
2026-04-15 11:42:49 +02:00
Andrey Antukh b26ef158ef 📚 Fix typos in vec2, zip-all, and map-perm docstrings 2026-04-15 11:42:49 +02:00
Andrey Antukh 95d4d42c91 🐛 Add missing string? guard to num-string? on JVM
The CLJS branch of num-string? checked (string? v) first, but the
JVM branch did not. Passing non-string values (nil, keywords, etc.)
would rely on exception handling inside parse-double for control
flow. Add the string? check for consistency and to avoid using
exceptions for normal control flow.
2026-04-15 11:42:49 +02:00
Andrey Antukh bba3610b7b ♻️ Rename shadowed 'fn' parameter to 'pred' in removev
The removev function used 'fn' as its predicate parameter name,
which shadows clojure.core/fn. Rename to 'pred' for clarity and
to follow the naming convention used elsewhere in the namespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh 83da487b24 🐛 Fix append-class producing leading space for empty class
When called with an empty string as the base class, append-class
was producing " bar" (with a leading space) because (some? "")
returns true. Use (seq class) instead to treat both nil and empty
string as absent, avoiding invalid CSS class strings with leading
whitespace.
2026-04-15 11:42:49 +02:00
Andrey Antukh da8e44147c Remove redundant str call in format-number
format-precision already returns a string, so wrapping its result
in an additional (str ...) call was unnecessary.
2026-04-15 11:42:49 +02:00
Andrey Antukh 69e25a4998 📚 Fix typo in namespace docstring ('if' -> 'of') 2026-04-15 11:42:49 +02:00
Andrey Antukh eca9b63d68 Remove redundant map lookups in map-diff
The :else branch of diff-attr was calling (get m1 key) and
(get m2 key) again, but v1 and v2 were already bound to those
exact values. Reuse the existing bindings to avoid the extra
lookups.
2026-04-15 11:42:49 +02:00
Andrey Antukh 29ea1cc495 📚 Fix misleading without-obj docstring
The docstring claimed the function removes nil values in addition to
the specified object, but the implementation only removes elements
equal to the given object. Fix the docstring in both data.cljc and
the local copy in files/changes.cljc.
2026-04-15 11:42:49 +02:00
Andrey Antukh d73ab3ec92 🐛 Fix safe-subvec 3-arity evaluating (count v) before nil check
The 3-arity of safe-subvec called (count v) in a let binding before
checking (some? v). While (count nil) returns 0 in Clojure and does
not crash, the nil guard was dead code. Restructure to check (some? v)
first with an outer when, then compute size inside the guarded block.
2026-04-15 11:42:49 +02:00
Andrey Antukh 1cc860807e Use seq/next idiom in enumerate instead of empty?/rest
Replace (empty? items) + (rest items) with (seq items) + (next items)
in enumerate. The seq/next pattern is idiomatic Clojure and avoids
the overhead of empty? which internally calls seq and then negates.
2026-04-15 11:42:49 +02:00
Andrey Antukh 92dd5d9954 🐛 Fix index-of-pred early termination on nil elements
The index-of-pred function used (nil? c) to detect end-of-collection,
which caused premature termination when the collection contained nil
values. Rewrite using (seq coll) / (next s) pattern to correctly
distinguish between nil elements and end-of-sequence.
2026-04-15 11:42:49 +02:00
Andrey Antukh 057c6ddc0d 🐛 Fix deep-mapm double-applying mfn on leaf entries
The deep-mapm function was applying the mapping function twice on
leaf entries (non-map, non-vector values): once when destructuring
the entry, and again on the already-transformed result in the else
branch. Now mfn is applied exactly once per entry.
2026-04-15 11:42:49 +02:00
Andrey Antukh a2e6abcb72 🐛 Fix spurious argument to dissoc in patch-object
The patch-object function was calling (dissoc object key value) when
handling nil values. Since dissoc treats each argument after the map
as a key to remove, this was also removing nil as a key from the map.
The correct call is (dissoc object key).
2026-04-15 11:42:49 +02:00
Andrey Antukh 6d1d044588 ♻️ Move app.common.types.color tests to their own namespace
Tests that exercise app.common.types.color were living inside
common-tests.colors-test alongside the app.common.colors tests. Move
them to common-tests.types.color-test so the test namespace mirrors
the source namespace structure, consistent with the rest of the
types/ test suite.

The [app.common.types.color :as colors] require is removed from
colors_test.cljc; the new file is registered in runner.cljc.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 1e0f10814e 🔥 Remove duplicate gradient helpers from app.common.colors
The five functions interpolate-color, offset-spread, uniform-spread?,
uniform-spread, and interpolate-gradient duplicated the canonical
implementations in app.common.types.color. The copies in colors.cljc
also contained two bugs: a division-by-zero in offset-spread when
num=1, and a crash on nil idx in interpolate-gradient.

All production callers already use app.common.types.color. The
duplicate tests that exercised the old copies are removed; their
coverage is absorbed into expanded tests under the types-* suite,
including a new nil-idx guard test and a single-stop no-crash test.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh db7c646568 Add missing tests for session bug fixes and uniform-spread?
Add indexed-access-with-default in fill_test.cljc to cover the two-arity
(nth fills i default) form on both valid and out-of-range indices, directly
exercising the CLJS Fills -nth path fixed in 593cf125.

Add segment-content->selrect-multi-line in path_data_test.cljc to cover
content->selrect on a subpath with multiple consecutive line-to commands
where move-p diverges from from-p, confirming the bounding box matches
both the expected coordinates and the reference implementation; this
guards the calculate-extremities fix in bb5a04c7.

Add types-uniform-spread? in colors_test.cljc to cover
app.common.types.color/uniform-spread?, which had no dedicated tests.
Exercises the uniform case (via uniform-spread), the two-stop edge case,
wrong-offset detection, and wrong-color detection.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh caac452cd4 🐛 Fix wrong extremity point in calculate-extremities for line-to
In the :line-to branch of calculate-extremities, move-p (the subpath
start point) was being added to the extremities set instead of from-p
(the actual previous point). For all line segments beyond the first one
in a subpath this produced an incorrect bounding-box start point.

The :curve-to branch correctly used from-p; align :line-to to match.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 30931839b5 🐛 Fix reversed d/in-range? args in CLJS Fills -nth with default
In the ClojureScript Fills deftype, the two-arity -nth implementation
called (d/in-range? i size) but the signature is (d/in-range? size i).
This meant -nth always fell through to the default value for any valid
index when called with an explicit default, since i < size is the
condition but the args were swapped.

The no-default -nth sibling on line 378 and both CLJ nth impls on
lines 286 and 291 had the correct argument order.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 6da39bc9c7 🐛 Fix ObjectsMap CLJS negative cache keyed on 'key' fn instead of 'k'
In the CLJS -lookup implementation, when a key is absent from data the
negative cache entry was stored under 'key' (the built-in map-entry
key function) rather than the 'k' parameter.  As a result every
subsequent lookup of any missing key bypassed the cache and repeated
the full lookup path, making the negative-cache optimization entirely
ineffective.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 2b67e114b6 🐛 Fix inside-layout? passing id instead of shape to frame-shape?
`(cfh/frame-shape? current-id)` passes a UUID to the single-arity
overload of `frame-shape?`, which expects a shape map; it always
returns false. Fix by passing `current` (the resolved shape) instead.
Update the test to assert the correct behaviour.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 8b08c8ecc9 🐛 Fix wrong mapcat call in collect-main-shapes
`(mapcat collect-main-shapes children objects)` passes `objects` as a
second parallel collection instead of threading it as the second
argument to `collect-main-shapes` for each child. Fix by using an
anonymous fn: `(mapcat #(collect-main-shapes % objects) children)`.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:09 +02:00
Andrey Antukh 8253738f01 🐛 Fix reversed get args in convert-dtcg-shadow-composite
\`(get "type" shadow)\` always returns nil because the map and key
arguments were swapped. The correct call is \`(get shadow "type")\`,
which allows the legacy innerShadow detection to work correctly.
Update the test expectation accordingly.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:25:03 +02:00
Andrey Antukh c30c85ff07 🐛 Remove duplicate font-weight-keys in typography-keys union
font-weight-keys was listed twice in the set/union call for
typography-keys, a copy-paste error. The duplicate entry has no
functional effect (sets deduplicate), but it is misleading and
suggests a missing key such as font-style-keys in its place.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh ff41d08e3c 🐛 Fix stale accumulator in get-children-in-instance recursion
get-children-rec passed the original children vector to each recursive
call instead of the updated one that already includes the current
shape. This caused descendant results to be accumulated from the wrong
starting point, losing intermediate shapes. Pass children' (which
includes the current shape) into every recursive call.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh 08ca561667 🐛 Add better nil handling in interpolate-gradient when offset exceeds stops
When no gradient stop satisfies (<= offset (:offset %)),
d/index-of-pred returns nil. The previous code called (dec nil) in
the start binding before the nil check, throwing a
NullPointerException/ClassCastException. Guard the start binding with
a cond that handles nil before attempting dec.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh 7b0ea5968d 🚑 Fix typo :podition in swap-shapes grid cell
The key :podition was used instead of :position when updating the
id-from cell in swap-shapes, silently discarding the position value
and leaving the cell's :position as nil after every swap.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-14 21:17:14 +02:00
Andrey Antukh 6c90ba1582 🐛 Fix move-files allowing same project as target when multiple files selected
The 'Move to' menu in the dashboard file context menu only filtered
out the first selected file's project from the available target list.
When multiple files from different projects were selected, the other
files' projects still appeared as valid targets, causing a 400
'cant-move-to-same-project' backend error.

Now all selected files' project IDs are collected and excluded from
the available target projects.
2026-04-14 15:19:19 +02:00
Andrey Antukh 18f0ad246f 🐛 Fix parse-long crash when index query param is duplicated in URL
lambdaisland/uri's query-string->map uses :multikeys :duplicates by
default: a key that appears once yields a plain string, but the same
key repeated yields a vector. cljs.core/parse-long only accepts
strings and therefore threw "Expected string, got: object" whenever
a URL contained a duplicate 'index' parameter.

Add rt/get-query-param to app.main.router. The helper returns the
scalar value of a query param key, taking the last element when the
value is a sequential (i.e. the key was repeated). Use it at every
call site that feeds a query-param value into parse-long, in both
app.main.ui (page*) and app.main.data.viewer.
2026-04-14 15:16:04 +02:00
Andrey Antukh 62f3454607 🔧 Backport ci configuration changes from develop 2026-04-14 12:34:04 +02:00
Andrey Antukh c39609b991 ♻️ Use shared singleton containers for React portals (#8957)
Refactor use-portal-container to allocate one persistent <div> per
logical category (:modal, :popup, :tooltip, :default) instead of
creating a new div for every component instance. This keeps the DOM
clean with at most four fixed portal containers and eliminates the
arbitrary growth of empty <div> elements on document.body while
preserving the removeChild race condition fix.
2026-04-14 10:48:30 +02:00
Andrey Antukh b3645658fb Merge pull request #8963 from penpot/raguirref-fix/builder-bool-media-validation
🐛 Fix builder bool styles and media validation
2026-04-13 18:35:12 +02:00
Andrey Antukh e46b34efc7 📎 Fix formatting issues 2026-04-13 15:41:38 +02:00
raguirrefandCopilot 94c6045dd9 🔥 Remove accidental dev_server.pid
Remove unrelated local pid file that was accidentally included in previous commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: raguirref <ricardoaguirredelafuente@gmail.com>
2026-04-13 15:40:40 +02:00
raguirrefandCopilot f656266e5c Fix builder bool and media handling
Fixes three concrete builder issues in common/files/builder:\n- Use bool type from shape when selecting style source for difference bools\n- Persist :strokes correctly (fix typo :stroks)\n- Validate add-file-media params after assigning default id\n\nAlso adds regression tests in common-tests.files-builder-test and registers them in runner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: raguirref <ricardoaguirredelafuente@gmail.com>
2026-04-13 15:40:40 +02:00
Andrey Antukh 0fc2050526 ⬆️ Update deps on root package.json 2026-04-13 15:00:47 +02:00
Andrey Antukh 28f65fec91 📚 Update changelog 2026-04-13 12:15:17 +02:00
Aitor Moreno 9c44f5bf65 🐛 Fix text editor v1 focus not being handled correctly (#8942) 2026-04-13 12:08:06 +02:00
Eva Marco 443fb60743 🐛 Fix highlight on frames after rename (#8938) 2026-04-13 12:04:04 +02:00
Luis de DiosandAndrey Antukh cbe9d31599 🐛 Fix dashboard navigation tabs overlap with content when scrolling (#8937)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-13 12:01:10 +02:00
Andrey Antukh d6045c80a1 💄 Fix docstrings and clarify filter expression in path namespaces
- Fix 'conten' typo to 'content' in path.cljc docstring
- Fix 'curvle' typo to 'curve' in shape_to_path.cljc docstring
- Replace confusing XOR-style filter with readable
  (contains? #{:line-to :curve-to} ...) in bool.cljc
- Align handler-indices and opposite-index docstrings with
  matching API in path.cljc
2026-04-13 11:48:30 +02:00
Andrey Antukh 8d1906f56e 🐛 Fix ^:cosnt typo to ^:const on bool-group-style-properties
The metadata key was misspelled as :cosnt instead of :const,
preventing the compiler from recognizing the Var as a compile-time
constant.
2026-04-13 11:48:30 +02:00
Andrey Antukh 2eaf117b56 🐛 Fix swapped arguments in CLJS PathData -nth with default
The CLJS implementation of PathData's -nth protocol method had
swapped arguments in the 3-arity version (with default value).
The call (d/in-range? i size) should be (d/in-range? size i)
to match the CLJ implementation. With swapped args, valid indices
always returned the default value, and invalid indices attempted
out-of-bounds buffer reads.
2026-04-13 11:48:30 +02:00
Andrey Antukh e511576f66 🐛 Normalize PathData coordinates to safe integer bounds on read
Add normalize-coord helper function that clamps coordinate values to
max-safe-int and min-safe-int bounds when reading segments from PathData
binary buffer. Applies normalization to read-segment, impl-walk,
impl-reduce, and impl-lookup functions to ensure coordinates remain
within safe bounds.

Add corresponding test to verify out-of-bounds coordinates are properly
clamped when reading PathData.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-13 11:48:30 +02:00
Andrey Antukh a403175d5c 🐛 Fix TypeError in sd-token-uuid when resolving tokens interactively (#8929)
The backtrace-tokens-tree function used a namespaced keyword :temp/id
which clj->js converted to the JS property "temp/id". The sd-token-uuid
function then tried to access .id on the sd-token top-level object,
which was undefined, causing "Cannot read properties of undefined
(reading uuid)".

Fix by using the existing token :id instead of generating a temporary
one, and read it from sd-token.original (matching sd-token-name pattern).
2026-04-13 11:34:15 +02:00
Pablo AlbaandAndrey Antukh ef6eeb5693 🐛 Fix variants corner cases with selrect and points (#8882)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-10 11:23:03 +02:00
Marek HrabeandAndrey Antukh 9785a13e67 🐛 Add webp export format to plugin types (#8870)
* 🐛 Add webp export format to plugin types

Align plugin API typings with runtime export support by including 'webp' in
'Export.type' and updating the exported formats documentation.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

* 📚 Add plugin-types changelog entry for missing webp export format

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

---------

Signed-off-by: Marek Hrabe <marekhrabe@me.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-10 11:22:20 +02:00
Elena Torró a88f8f1394 Merge pull request #8918 from penpot/niwinz-main-path-preview-issue
🐛 Fix path drawing preview passing shape instead of content to next-node
2026-04-09 11:48:58 +02:00
Andrey Antukh 388775413e 🐛 Fix path drawing preview passing shape instead of content to next-node
In `preview-next-point`, `st/get-path` was called without extra keys,
which returns the full Shape record. That value was then passed directly
to `path/next-node` as its `content` argument.

`path/next-node` delegates to `impl/path-data`, which only accepts a
`PathData` instance, `nil`, or a sequential collection of segments. A
Shape record matches none of those cases, so `path-data` threw
"unexpected data" every time the user moved the mouse while drawing a
path.

The fix is to call `(st/get-path state :content)` so that only the
`:content` field (a `PathData` instance) is extracted and forwarded to
`path/next-node`.
2026-04-09 09:21:57 +00:00
Andrey Antukh dfa45ec8d8 ⬆️ Update deps on root package.json 2026-04-09 09:10:44 +02:00
andrés gonzález 6063c1c532 :books:Clarify remote MCP availability in production (#8910) 2026-04-08 17:48:05 +02:00
Andrey Antukh ffac8d2861 📎 Update changelog 2026-04-08 17:34:00 +02:00
Andrey Antukh f97df3e8ab 🐛 Fix PathData corruption root causes across WASM and CLJS
Replace unsafe std::mem::transmute calls in Rust WASM path code with
validated TryFrom conversions to prevent undefined behavior from invalid
enum discriminant values. This was the most likely root cause of the
"No matching clause: -19772" production crash -- corrupted bytes flowing
through transmute could produce arbitrary invalid enum variants.

Fix byteOffset handling throughout the CLJS PathData serialization
pipeline. DataView instances created via buf/slice carry a non-zero
byteOffset, but from-bytes, transit write handler, -write-to,
buf/clone, and buf/equals? all operated on the full underlying
ArrayBuffer, ignoring offset and length. This could silently produce
PathData with incorrect size or content.

Rust changes (render-wasm):
- RawSegmentData: From<[u8; N]> -> TryFrom<[u8; N]> with discriminant
  validation (must be 0x01-0x04) before transmuting
- RawBoolType: From<u8> -> TryFrom<u8> with explicit match on 0-3
- Add #[wasm_error] to set_shape_path_content, current_to_path,
  convert_stroke_to_path, and set_shape_bool_type so panics are caught
  and routed through the WASM error protocol instead of crashing
- set_shape_path_content: replace .expect() with proper Result/? error
  propagation per segment
- Remove unused From<BytesType> bound from SerializableResult trait

CLJS changes (common):
- from-bytes: use DataView.byteLength instead of ArrayBuffer.byteLength
  for DataView inputs; preserve byteOffset/byteLength when converting
  from Uint8Array, Uint32Array, and Int8Array
- Transit write handler: construct Uint8Array with byteOffset and
  byteLength from the DataView, not the full backing ArrayBuffer
- -write-to: same byteOffset/byteLength fix
- buf/clone: copy only the DataView byte range using Uint8Array with
  proper offset, not Uint32Array over the full ArrayBuffer
- buf/equals?: compare DataView byte ranges using Uint8Array with
  proper offset, not the full backing ArrayBuffers

Frontend changes:
- shape-to-path, stroke-to-path, calculate-bool*: wrap WASM call and
  buffer read in try/catch to ensure mem/free is always called, even
  when an exception occurs between the WASM call and the free call

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-08 17:24:09 +02:00
Andrey Antukh b63e4a297b 🐛 Handle corrupted PathData segments gracefully instead of crashing
Add nil defaults to all case expressions that match binary segment
type codes so that corrupted/unknown values are skipped instead of
throwing 'No matching clause'. This prevents a React render crash
(triggered via shape-with-open-path? -> get-subpaths -> reduce)
when a PathData buffer contains invalid bytes, e.g. from a WASM
data transfer or deserialization of damaged stored data.

Affected functions: read-segment, impl-walk, impl-reduce,
impl-lookup, to-string-segment*, and the seq/reduce protocol
implementations on both JVM and CLJS PathData types.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-08 17:21:03 +02:00
Andrey Antukh 6a0d131715 🐛 Fix swapped move-to/line-to type codes in PathData binary readers
The impl-walk, impl-reduce, and impl-lookup functions had the binary
type codes for move-to and line-to swapped (1 mapped to :line-to and
2 to :move-to). This is inconsistent with from-plain, read-segment,
to-string-segment*, and the Rust RawSegmentData enum which all use
1=move-to and 2=line-to. The swap caused incorrect command types to
be reported to callers like get-handlers and get-points.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-08 17:20:47 +02:00
Andrey Antukh cb33fe417e 🐛 Fix non-integer row/column values in grid cell position inputs (#8869)
* 🐛 Fix non-integer row/column values in grid cell position inputs

The numeric-input component allows Alt+arrow key increments of 0.1x the
step value, which could produce float values (e.g. 4.5, 0.5) when users
adjusted grid cell row/column/row-span/column-span positions. The schema
requires these fields to be integers, causing backend validation errors.

Round the input values to integers in the on-grid-coordinates callback
before passing them to update-grid-cell-position.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Enforce integer-only values in grid cell numeric inputs

Add an `integer` prop to the legacy `numeric-input*` component that
rounds parsed values in `parse-value`, ensuring all input paths (typed
text, arrow keys, Alt+arrow, mouse wheel, expressions) produce integers.
Use it for all six row/column inputs in the grid cell options panel.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-08 17:05:55 +02:00
Andrey Antukh c8675c5b7e ♻️ Normalize newsletter-updates checbox on different register flows (#8839)
*  Add newsletter opt-in checkbox to registration validation form

Add accept-newsletter-updates support through the full registration
token flow. The newsletter checkbox is now available on the
registration validation form, allowing users to opt-in during the
email verification step.

Backend changes:
- Refactor prepare-register to consolidate UTM params and newsletter
  preference into props at token creation time
- Add accept-newsletter-updates to prepare-register-profile and
  register-profile schemas
- Handle newsletter-updates in register-profile by updating token
  claims props on second step

Frontend changes:
- Add newsletter-options component to register-validate-form
- Add accept-newsletter-updates to validation schema
- Fix subscription finalize/error handling in register form

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Refactor auth register components to modern style

Migrate all components in app.main.ui.auth.register and
app.main.ui.auth.login/demo-warning to use the modern * suffix
convention, removing deprecated ::mf/props :obj metadata and
updating all invocations from [:& name] to [:> name*] syntax.

Components updated:
- terms-and-privacy -> terms-and-privacy*
- register-form -> register-form*
- register-methods -> register-methods*
- register-page -> register-page*
- register-success-page -> register-success-page*
- terms-register -> terms-register*
- register-validate-form -> register-validate-form*
- register-validate-page -> register-validate-page*
- demo-warning -> demo-warning*

Also remove unused old context-notification import in login.cljs.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🔥 Remove unused onboarding-newsletter component

The newsletter opt-in is now handled directly in the registration
form via the newsletter-options* component, making the standalone
onboarding-newsletter modal obsolete.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Fix register test for UTM params to use prepare-register step

UTM params are now extracted and stored in token props during the
prepare-register step, not at register-profile time. Move utm_campaign
and mtm_campaign from the register-profile call to the
prepare-register-profile call in the test.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-08 17:00:52 +02:00
Andrey Antukh f8c04949e1 🐛 Fix nil path content crash by exposing safe public API (#8806)
* 🐛 Fix nil path content crash by exposing safe public API

Move nil-safety for path segment helpers to the public API layer
(app.common.types.path) rather than the low-level segment namespace.
Add nil-safe wrappers for get-handlers, opposite-index, get-handler-point,
get-handler, handler->node, point-indices, handler-indices, next-node,
append-segment, points->content, closest-point, make-corner-point,
make-curve-point, split-segments, remove-nodes, merge-nodes, join-nodes,
and separate-nodes. Update all frontend callers to use path/ instead of
path.segment/ for these functions, removing the path.segment require
from helpers, drawing, edition, tools, curve, editor and debug.

Replace ad-hoc nil checks with impl/path-data coercion in all public
wrapper functions in app.common.types.path. The path-data helper
already handles nil by returning an empty PathData instance, which
provides uniform nil-safety across all content-accepting functions.

Update the path-get-points-nil-safe test to expect empty collection
instead of nil, matching the new coercion behavior.

* ♻️ Clean up path segment dead code and add missing tests

Remove dead code from segment.cljc: opposite-handler (duplicate of
calculate-opposite-handler) and path-closest-point-accuracy (unused
constant). Make update-handler and calculate-extremities private as
they are only used internally within segment.cljc.

Add missing tests for path/handler-indices, path/closest-point,
path/make-curve-point and path/merge-nodes. Update extremities tests
to use the local reference implementation instead of the now-private
calculate-extremities. Remove tests for deleted/privatized functions.

Add empty-content guard in path/closest-point wrapper to prevent
ArityException when reducing over zero segments.
2026-04-07 18:54:14 +02:00
Andrey Antukh e10bd6a8d3 🐛 Fix infinite recursion in get-frame-ids for thumbnail extraction (#8807)
The get-frame-ids function could enter infinite recursion when:
1. There's a circular reference in the frame hierarchy
2. A shape's frame-id points to itself (corrupt data)

The fix uses the cached version (get-frame-ids-cached) in recursive calls
and adds a guard to prevent self-referencing.
2026-04-07 16:34:08 +02:00
Andrey Antukh 52f28a1eee 🐛 Fix stale-asset detector missing protocol-dispatch errors
The stale-asset-error? predicate only matched keyword-constant
cross-build mismatches ($cljs$cst$). Protocol dispatch failures
($cljs$core$I prefix, e.g. IFn/ISeq) and V8's 'Cannot read
properties of undefined' phrasing were not covered, so the handler
fell through to a generic toast instead of triggering a hard reload.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-07 16:33:40 +02:00
Andrey Antukh 9a0ae32488 ⬆️ Update opencode dependency on repo root 2026-04-07 16:33:40 +02:00
Andrey Antukh 1e4ff4aa47 🐛 Ignore Zone.js toString TypeError in uncaught error handler (#8804)
Zone.js (injected by browser extensions such as Angular DevTools) patches
addEventListener by wrapping it and assigning a custom .toString to the
wrapper via Object.defineProperty with writable:false.  When the same
element is processed a second time, the plain assignment in strict mode
(libs.js is built with a "use strict" banner) throws a native TypeError:
"Cannot assign to read only property 'toString' of function '...'".

This error escapes the React tree through the window error/unhandledrejection
events and was surfacing the exception page to users even though Penpot itself
is unaffected.

The fix:
- Extract the private ignorable-exception? helpers from the letfn block into
  top-level defn/defn- forms so the predicate can be reused elsewhere.
- Add the Zone.js toString TypeError to the ignorable-exception? predicate so
  the global uncaught-error handler silently suppresses it.
- The React error boundary is intentionally left unchanged: anything that
  reaches it has executed inside React's reconciler and must not be ignored.
2026-04-07 16:25:57 +02:00
Andrey Antukh b99157a246 🐛 Prevent thumbnail frame recursion overflow (#8763)
Cache in-progress frame traversals before following parent frame links so thumbnail updates stop recursing forever on cyclic or transiently inconsistent shape graphs.

Add a regression test that covers cyclic frame-id chains and keeps the expected frame/component extraction behavior intact.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-07 15:09:54 +02:00
Andrey AntukhandLuis de Dios 2ca7acfca6 Add tests for app.common.geom and descendant namespaces (#8768)
* 🎉 Add tests for app.common.geom.bounds-map

* 🎉 Add tests for app.common.geom and descendant namespaces

* 📎 Fix linting issues

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-04-02 09:50:34 +02:00
Andrey Antukh d2a3b67053 🎉 Add additional tests for app.common.types.shape.interactions (#8765)
*  Expand interaction helper test coverage

Add coverage for interaction destination and flow helpers,
including nil handling and removal helpers. Document the
intent of the new assertions so future interaction changes
keep the helper contract explicit.

*  Cover interaction validation edge cases

Exercise the remaining interaction guards and overlay
positioning edge cases, including invalid state
transitions and nested manual offsets. Keep the test
comments focused on why each branch matters for editor
 behavior.
2026-04-02 09:50:08 +02:00
Andrey Antukh 3ff1acfb6a 🐛 Fix vector index out of bounds in viewer zoom-to-fit/fill (#8834)
Clamp the frame index to the valid range in zoom-to-fit and
zoom-to-fill events before accessing the frames vector. When the
URL query parameter :index exceeds the number of frames on the
page (e.g. index=1 with a single frame), nth would throw
"No item 1 in vector of length 1". Also adds unit tests covering
the boundary condition.
2026-04-02 09:49:33 +02:00
Andrey Antukh 81b1b253f1 Add unique email domains to telemetry report (#8819)
Extend the telemetry payload with a sorted list of unique email domains
extracted from all registered profile email addresses. The new
:email-domains field is populated via a single SQL query using
split_part and DISTINCT, and is included in the stats sent when
telemetry is enabled.

Also update the tasks-telemetry-test to assert the new field is present
and contains the expected domain values.
2026-04-01 11:49:50 +02:00
Andrey Antukh 0337607a1b 🐛 Guard delete undo against missing sibling order (#8858)
Return nil from get-prev-sibling when the shape is no longer present in
the parent ordering so delete undo generation falls back to index-based
restore instead of crashing on invalid vector access.
2026-04-01 11:49:17 +02:00
Belén Albeza 8c1cf3623b 🔧 Update action checkout to v6 (#8861) 2026-04-01 11:29:55 +02:00
Andrey Antukh d3ac824912 🐛 Fix ICounted error on numeric-input token dropdown keyboard nav (#8803)
The options stored in options-ref is a delay (lazy value). In
on-token-key-down, it was passed raw to next-focus-index without being
dereferenced first, causing count to be called on a JS object that does
not implement ICounted.

Fix: dereference the delay in on-token-key-down (matching the existing
pattern in on-key-down), and make next-focus-index itself also handle
delays defensively. Add unit tests covering the delay case.
2026-04-01 11:21:01 +02:00
Andrey Antukh 350cc01b72 🐛 Fix frontend test script 2026-04-01 11:10:28 +02:00
Andrey Antukh 8289120ea4 Replace pnpx with pnpm exec in render-wasm build script
The pnpx tries to fetch esbuild instead of using the already installed
version.
2026-04-01 11:10:28 +02:00
Yamila Moreno 084ca401fd 📚 Improve recommended settings for self-host (#8846) 2026-03-31 15:11:58 +02:00
Andrey Antukh c1044ac522 Add protection for stale cache of js assets loading issues (#8638)
*  Use update-when for update dashboard state

This make updates more consistent and reduces possible eventual
consistency issues in out of order events execution.

* 🐛 Detect stale JS modules at boot and force reload

When the browser serves cached JS files from a previous deployment
alongside a fresh index.html, code-split modules reference keyword
constants that do not exist in the stale shared.js, causing TypeError
crashes.

This adds a compile-time version tag (via goog-define / closure-defines)
that is baked into the JS bundle. At boot, it is compared against the
runtime version tag from index.html (which is always fresh due to
no-cache headers). If they differ, the app forces a hard page reload
before initializing, ensuring all JS modules come from the same build.

* 📎 Ensure consistent version across builds on github e2e test workflow

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-30 19:46:51 +02:00
Andrey Antukh ef3143dcb8 📎 Update changelog 2026-03-30 12:35:39 +02:00
Andrey Antukh 3767ee05bb Add retry mechanism for idenpotent get repo requests on frontend (#8792)
* ♻️ Handle fetch-error gracefully with toast instead of full-page error

Network-level failures (lost connectivity, DNS failure, etc.) on RPC
calls were propagating as :internal/:fetch-error to the global error
handler, which replaced the entire UI with a full-page error screen.

Now the :internal handler distinguishes :fetch-error from other internal
errors and shows a non-intrusive toast notification instead, allowing
the user to continue working.

*  Add automatic retry with backoff for idempotent RPC requests

Idempotent (GET) RPC requests are now automatically retried up to 3
times with exponential back-off (1s, 2s, 4s) when a transient error
occurs.  Retryable errors include: network-level failures
(:fetch-error), 502 Bad Gateway, 503 Service Unavailable, and browser
offline (status 0).

Mutation (POST) requests are never retried to avoid unintended
side-effects.  Non-transient errors (4xx client errors, auth errors,
validation errors) propagate immediately without retry.

* ♻️ Make retry helpers public with configurable parameters

Make retryable-error? and with-retry public functions, and replace
private constants with a default-retry-config map.  with-retry now
accepts an optional config map (:max-retries, :base-delay-ms) enabling
callers and tests to customize retry behavior.

*  Add tests for RPC retry mechanism

Comprehensive tests for the retry helpers in app.main.repo:
- retryable-error? predicate: covers all retryable types (fetch-error,
  bad-gateway, service-unavailable, offline) and non-retryable types
  (validation, authentication, authorization, plain errors)
- with-retry observable wrapper: verifies immediate success, recovery
  after transient failures, max-retries exhaustion, no retry for
  non-retryable errors, fetch-error retry, custom config, and mixed
  error scenarios

* ♻️ Introduce :network error type for fetch-level failures

Replace the awkward {:type :internal :code :fetch-error} combination
with a proper {:type :network} type in app.util.http/fetch.  This makes
the error taxonomy self-explanatory and removes the special-case branch
in the :internal handler.

Consequences:
- http.cljs: emit {:type :network} instead of {:type :internal :code :fetch-error}
- errors.cljs: add a dedicated ptk/handle-error :network method (toast);
  restore :internal handler to its original unconditional full-page error form
- repo.cljs: simplify retryable-types and retryable-error? — :network
  replaces the former :internal special-case, no code check needed
- repo_test.cljs: update tests to use {:type :network}

* 📚 Add comment explaining the use of bit-shift-left
2026-03-30 12:20:02 +02:00
Andrey Antukh e7e98255d9 Add scroll and zoom raf throttling (#8812)
* ⬆️ Update opencode and copilot deps

* 🐛 Decouple workspace-content from workspace-local to reduce scroll re-renders

Move workspace-local subscription from workspace-content* (parent) into
viewport* and viewport-classic* (children). workspace-content* now only
subscribes to the new workspace-vport derived atom, which changes only on
window resize — not on every scroll event. This prevents the sidebar,
palette and other workspace-content children from re-rendering on scroll.

* 🐛 Throttle wheel events to one state update per animation frame

Accumulate wheel event deltas in a mutable ref and flush them via
requestAnimationFrame, so that multiple wheel events between frames
produce a single state mutation instead of one per event. This prevents
the cascade of synchronous React re-renders (via useSyncExternalStore)
that can exceed the maximum update depth on rapid scrolling.

Both panning (scroll) and zoom (ctrl/mod+wheel) are throttled. Scroll
deltas are summed additively; zoom scales are compounded multiplicatively
with the latest cursor point used as the zoom center.

* ♻️ Extract schedule-zoom! and schedule-scroll! from on-mouse-wheel

* ♻️ Avoid zoom dep on on-mouse-wheel by using a ref
2026-03-30 12:06:56 +02:00
Andrey Antukh d7e0b0cf9f 📎 Add check-fmt script to root package.json 2026-03-30 11:06:13 +02:00
Andrey Antukh b6524881e0 🐛 Fix crash in apply-text-modifier with nil selrect or modifier (#8762)
* 🐛 Fix crash in apply-text-modifier with nil selrect or modifier

Guard apply-text-modifier against nil text-modifier and nil selrect
to prevent the 'invalid arguments (on pointer constructor)' error
thrown by gpt/point when called with an invalid map.

- In text-wrapper: only call apply-text-modifier when text-modifier is
  not nil (avoids unnecessary processing)
- In apply-text-modifier: handle nil text-modifier by returning shape
  unchanged; guard selrect access before calling gpt/point

* 📚 Add tests for apply-text-modifier in workspace texts

Add exhaustive unit tests covering all paths of apply-text-modifier:
- nil modifier returns shape unchanged (identity)
- modifier with no recognised keys leaves shape unchanged
- :width / :height modifiers resize shape correctly
- nil :width / :height keys are skipped
- both dimensions applied simultaneously
- :position-data is set and nil-guarded
- position-data coordinates translated by delta on resize
- shape with nil selrect + nil modifier does not throw
- position-data-only modifier on shape without selrect is safe
- selrect origin preserved when no dimension changes
- result always carries required shape keys

* 🐛 Fix zero-dimension selrect crash in change-dimensions-modifiers

When a text shape is decoded from the server via map->Rect (which
bypasses make-rect's 0.01 minimum enforcement), its selrect can have
width or height of exactly 0.  change-dimensions-modifiers and
change-size were dividing by these values, producing Infinity scale
factors that propagated through the transform pipeline until
calculate-selrect / center->rect returned nil, causing gpt/point to
throw 'invalid arguments (on pointer constructor)'.

Fix: before computing scale factors, guard sr-width / sr-height (and
old-width / old-height in change-size) against zero/negative and
non-finite values.  When degenerate, fall back to the shape's own
top-level :width/:height so the denominator and proportion-lock base
remain consistent.

Also simplify apply-text-modifier's delta calculation now that the
transform pipeline is guaranteed to produce a valid selrect, and
update the test suite to test the exact degenerate-selrect scenario
that triggered the original crash.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Simplify change-dimensions-modifiers internal logic

- Remove the intermediate 'size' map ({:width sr-width :height sr-height})
  that was built only to be assoc'd and immediately destructured back into
  width/height; compute both values directly instead.

- Replace the double-negated condition 'if-not (and (not ignore-lock?) …)'
  with a clear positive 'locked?' binding, and flatten the three-branch
  if-not/if tree into two independent if expressions keyed on 'attr'.

- Call safe-size-rect once and reuse its result for both the fallback
  sizes and the scale computation, eliminating a redundant call.

- Access :transform and :transform-inverse via direct map lookup rather
  than destructuring in the function signature, consistent with how the
  rest of the let-block reads shape keys.

- Clean up change-size to use the same destructuring style as the updated
  function ({sr-width :width sr-height :height}).

- Fix typo in comment: 'havig' -> 'having'.

*  Add tests for change-size and change-dimensions-modifiers

Cover the main behavioural contract of both functions:

change-size:
- Scales both axes to the requested target dimensions.
- Sets the resize origin to the shape's top-left point.
- Nil width/height each fall back to the current dimension (scale 1 on
  that axis); both nil produces an identity resize that is optimised away.
- Propagates the shape's transform and transform-inverse matrices into the
  resulting GeometricOperation.

change-dimensions-modifiers:
- Changing :width without proportion-lock only scales the x-axis (y
  scale stays 1), and vice-versa for :height.
- With proportion-lock enabled, changing :width adjusts height via the
  inverse proportion, and changing :height adjusts width via the
  proportion.
- ignore-lock? true bypasses proportion-lock regardless of shape state.
- Values below 0.01 are clamped to 0.01 before computing the scale.
- End-to-end: applying the returned modifiers via gsh/transform-shape
  yields the expected selrect dimensions.

*  Harden safe-size-rect with additional fallbacks

The previous implementation could still return an invalid rect in several
edge cases.  The new version tries four sources in order, accepting each
only if it passes a dedicated safe-size-rect? predicate:

1. :selrect           – used when width and height are finite, positive
                        and within [-max-safe-int, max-safe-int].
2. points->rect       – computed from the shape corner points; subject to
                        the same predicate.
3. Top-level shape fields (:x :y :width :height) – present on all rect,
                        frame, image, and component shape types.
4. grc/empty-rect     – a 0,0 0.01×0.01 unit rect used as last resort so
                        callers always receive a usable, non-crashing value.

The out-of-range check (> max-safe-int) is new: it rejects coordinates
that pass d/num? (finite) but exceed the platform integer boundary defined
in app.common.schema, which previously slipped through undetected.

Tests cover all four fallback paths, including the NaN, zero-dimension,
and max-safe-int overflow cases.

*  Optimise safe-size-rect for ClojureScript performance

- Replace (when (some? rect) ...) with (and ^boolean (some? rect) ...)
  to keep the entire predicate as a single boolean expression without
  introducing an implicit conditional branch.

- Replace keyword access (:width rect) / (:height rect) with
  dm/get-prop calls, consistent with the hot-path style used throughout
  the rest of the namespace.

- Add ^boolean type hints to every sub-expression of the and chain in
  safe-size-rect? (d/num?, pos?, <=) so the ClojureScript compiler emits
  raw JS boolean operations instead of boxing the results through
  cljs.core/truth_.

- Replace (when (safe-size-rect? ...) value) in safe-size-rect with
  (and ^boolean (safe-size-rect? ...) value), avoiding an extra
  conditional and keeping the or fallback chain free of allocated
  intermediate objects.

*  Use safe-size-rect in apply-text-modifier delta-move computation

safe-size-rect was already used inside change-dimensions-modifiers to
guard the resize scale computation. However, apply-text-modifier in
texts.cljs was still reading (:selrect shape) and (:selrect new-shape)
directly to build the delta-move vector via gpt/point.

gpt/point raises "invalid arguments (on pointer constructor)" when
given a nil value or a map with non-finite :x/:y, which can happen when
a shape's selrect is missing or degenerate (e.g. decoded from the server
via map->Rect, bypassing make-rect's 0.01 floor).

Changes:
- Promote safe-size-rect from defn- to defn in app.common.types.modifiers
  so it can be reused by consumers outside the namespace.
- Replace the two raw (:selrect …) accesses in the delta-move computation
  with (ctm/safe-size-rect …), which always returns a valid, finite rect
  through the established four-step fallback chain.
- Add two frontend tests covering the delta-move path with a fully
  degenerate (zero-dimension) selrect, ensuring neither a bare
  position-data modifier nor a combined width+position-data modifier
  throws.

* ♻️ Ensure all test shapes are proper Shape records in modifiers-test

All shapes in safe-size-rect-fallbacks tests now start from a proper
Shape record built by cts/setup-shape (via make-shape) instead of plain
hash-maps. Each test that mutates geometry fields (selrect, points,
width, height) does so via assoc on the already-initialised record,
which preserves the correct type while isolating the field under test.

A (cts/shape? shape) assertion is added to each fallback test to make
the type guarantee explicit and guard against regressions.

The unused shape-with-selrect helper (which built a bare map) is
removed.

* 🔥 Remove dead code and tighten visibility in app.common.types.modifiers

Dead functions removed (zero callers across the entire codebase):
- modifiers->transform-old: superseded by modifiers->transform; only
  ever appeared in a commented-out dev/bench.cljs entry.
- change-recursive-property: no callers anywhere.
- move-parent-modifiers, resize-parent-modifiers: convenience wrappers
  for the parent-geometry builder functions; never called.
- remove-children-modifiers, add-children-modifiers,
  scale-content-modifiers: single-op convenience builders; never called.
- select-structure: projection helper; only referenced by
  select-child-geometry-modifiers which is itself dead.
- select-child-geometry-modifiers: no callers anywhere.

Functions narrowed from defn to defn- (used only within this namespace):
- valid-vector?: assertion helper called only by move/resize builders.
- increase-order: called only by add-modifiers.
- transform-move!, transform-resize!, transform-rotate!, transform!:
  steps of the modifiers->transform pipeline.
- modifiers->transform1: immediate helper for modifiers->transform; the
  doc-string describing it as 'multiplatform' was also removed since it
  is an implementation detail.
- transform-text-node, transform-paragraph-node: leaf helpers for
  scale-text-content.
- update-text-content, scale-text-content, apply-scale-content: internal
  scale-content pipeline; all called only by apply-modifier.
- remove-children-set: called only by apply-modifier.
- select-structure: demoted to defn- rather than deleted because it is
  still called by select-child-structre-modifiers, which has external
  callers.

*  Add more tests for modifiers

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-30 11:04:54 +02:00
Andrey Antukh a149f31d56 Add comprehensive tests for shape layout namespace (#8759)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-30 11:03:53 +02:00
Andrey Antukh e4cc7d72da 🐛 Fix incorrect attrs references on generate-sync-shape (#8776)
For :component
2026-03-30 11:03:34 +02:00
Andrey Antukh 4174d6a05b 🎉 Add tests for undo-stack helper function on common (#8766) 2026-03-26 19:44:49 +01:00
Andrey Antukh 6db3c6cf89 🐛 Fix regression on subpath support (#8793) 2026-03-26 15:43:30 +01:00
Andrey Antukh 0dfa62a5b6 🐛 Improve error reporting on request parsing failures (#8805)
Include request URI and status in frontend handle-response error data,
and add request path/context to backend IOException handler logs and
response body. Previously these errors had no identifying information
about which endpoint or request caused the failure.
2026-03-26 15:42:49 +01:00
Penpot Dev 0ad3ae0620 📚 Add explicit commit guideline to builtin agents 2026-03-26 14:42:22 +01:00
Andrey AntukhandPenpot Dev 3eaf67a385 🐛 Fix fetch abort errors escaping the unhandled exception handler (#8801)
When AbortController.abort(reason) is called with a custom reason (a
ClojureScript ExceptionInfo), modern browsers (Chrome 98+, Firefox 97+)
reject the fetch promise with that reason object directly instead of with
the canonical DOMException{name:'AbortError'}.  The ExceptionInfo has
.name === 'Error', so both the p/catch guard and is-ignorable-exception?
failed to recognise it as an abort, letting it surface to users as an
error toast.

Fix by calling .abort() without a reason so the browser always produces
a native DOMException whose .name is 'AbortError', which is correctly
handled by all existing guards.

Also add a defense-in-depth check in is-ignorable-exception? that
filters errors whose message matches the 'fetch to \'' prefix, guarding
against any future re-introduction of a custom abort reason.

Co-authored-by: Penpot Dev <dev@penpot.app>
2026-03-26 14:13:38 +01:00
Andrey Antukh 1a4ca6d04b 📚 Update frontend/AGENTS.md file 2026-03-26 14:12:11 +01:00
Penpot Dev 945efdb0b4 🔥 Remove .opencode/skills
I think they make ai agent work worse.
2026-03-26 13:09:31 +01:00
Penpot Dev 2ba3605f11 ⬆️ Update root repo deps 2026-03-26 13:09:31 +01:00
Andrey Antukh 5fca9457cf ♻️ Extract use-portal-container hook to reduce duplication (#8798)
The dedicated-container portal pattern was repeated across 6 components.
Extract it into a reusable use-portal-container hook under app.main.ui.hooks.
2026-03-26 12:45:42 +01:00
Alejandro Alonso 85cfb8161a 📎 Rename skills 2026-03-25 23:50:29 +01:00
Alejandro Alonso a34a668f94 📎 Add opencode skills 2026-03-25 23:48:18 +01:00
andrés gonzález f01bfb7a26 🐛 Adding missing images to mcp doc (#8782) 2026-03-25 16:30:47 +01:00
Andrey Antukh 737e04fe2c 🐛 Fix nil deref on missing bounds in layout modifier propagation (#8735)
* 🐛 Fix nil deref on missing bounds in layout modifier propagation

When a parent shape has a child ID in its shapes vector that does
not exist in the objects map, the layout modifier code crashes
because it derefs nil from the bounds map.

The root cause is that children from the parent shapes list are
not validated against the objects map before being passed to the
layout modifier pipeline. Children with missing IDs pass through
unchecked and reach apply-modifiers where bounds lookup fails.

Fix by adding nil guards in apply-modifiers to skip children
without bounds, and changing map to keep to filter them out.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Add tests for nil bounds in layout modifier propagation

Tests cover flex and grid layout scenarios where a parent
frame has child IDs in its shapes vector that do not exist
in the objects map, verifying that set-objects-modifiers
handles these gracefully without crashing.

Tests:
- Flex layout with normal children (baseline)
- Flex layout with non-existent child in shapes
- Flex layout with only non-existent children
- Grid layout with non-existent child in shapes
- Flex layout resize propagation with ghost children
- Nested flex layout with non-existent child in outer frame

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-25 15:36:21 +01:00
andrés gonzález 38bf6c3603 📚 Add MCP docs (#8772) 2026-03-25 15:32:24 +01:00
Andrey Antukh a2672a598c 🐛 Fix TypeError when token error map lacks :error/fn key (#8767)
* 🐛 Fix TypeError when token error map lacks :error/fn key

Guard against missing :error/fn in token form control resolve streams.
When schema validation errors are produced they may not carry an
:error/fn key; calling nil as a function caused a TypeError crash.
Apply an if-let guard at all 7 affected sites across input.cljs,
color_input.cljs and fonts_combobox.cljs, falling back to :message
or returning the error map unchanged.

* ♻️ Extract token error helpers and add unit tests

Extract resolve-error-message and resolve-error-assoc-message helpers
into errors.cljs, replacing the seven duplicated inline lambdas in
input.cljs, color_input.cljs and fonts_combobox.cljs with named
function references.  Add frontend-tests.tokens.token-errors-test
covering both helpers for the normal path (:error/fn present) and the
fallback path (schema-validation errors that lack :error/fn).

Signed-off-by: Penpot Dev <dev@penpot.app>

---------

Signed-off-by: Penpot Dev <dev@penpot.app>
2026-03-25 12:12:18 +01:00
Andrey Antukh 0dfac801a4 Improve error handling and exception formatting (#8757)
*  Improve error handling and exception formatting

- Enhance exception formatting with visual separators and cause chaining
- Add new handler for :internal error type
- Refine error types: change assertion-related errors to :assertion type
- Improve error messages and hints consistency
- Clean up error handling in zip utilities and HTTP modules

* 🐛 Properly handle AbortError on fetch request unsubscription

When a fetch request in-flight is cancelled due to RxJS unsubscription
(e.g. navigating away from the workspace while thumbnail loads are
pending), the AbortController.abort() call triggers a catch handler
that previously relied solely on a @unsubscribed? flag to suppress the
error.

This was unreliable: nested observables spawned inside rx/mapcat (such
as datauri->blob-uri conversions within get-file-object-thumbnails)
could abort independently, with their own AbortController instances,
meaning the outer unsubscribed? flag was never set and the AbortError
propagated as an unhandled exception.

Add an explicit AbortError name check as a disjunctive condition so
that abort errors originating from any observable in the chain are
suppressed at the source, regardless of subscription state.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 19:55:23 +01:00
Andrey Antukh cc73a768d5 Add comprehensive tests for path and descendant namespaces (#8755)
Add tests for app.common.types.path.subpath, helpers, segment,
bool operations (union/difference/intersection/exclude), top-level
path API, and shape-to-path conversion. Covers previously untested
functions across all path sub-namespaces. Tests pass on both JVM
and JS (ClojureScript/Node) platforms.
2026-03-24 19:53:22 +01:00
Andrey Antukh 3ef100427b 🎉 Add tests for app.common.data namespace (#8750)
*  Add tests for predicates and ordered data structures

Adds tests for boolean-or-nil?, in-range?, ordered-set/map creation
and ordering, oassoc/oassoc-in/oupdate-in/oassoc-before, and the
ordered collection index helpers (adds/inserts/addm/insertm-at-index).

*  Add tests for lazy and sequence helpers

Adds tests for concat-all, mapcat, zip, zip-all, enumerate,
interleave-all, add-at-index, take-until, safe-subvec and domap.

*  Add tests for collection lookup and map manipulation

Adds tests for group-by, seek, index-by, index-of-pred/of,
replace-by-id, getf, vec-without-nils, without-nils,
without-qualified, without-keys, deep-merge, dissoc-in, patch-object,
without-obj, update-vals, update-in-when, update-when, assoc-in-when,
assoc-when, merge, txt-merge, mapm, removev, filterm, removem,
map-perm, distinct-xf and deep-mapm.

*  Add tests for parsing, numeric and utility helpers

Adds tests for nan?, safe+, max, min, parse-integer, parse-double,
parse-uuid, coalesce-str, coalesce, read-string, name, prefix-keyword,
kebab-keys, regexp?, nilf, nilv, any-key?, tap, tap-r, map-diff,
unique-name, toggle-selection, invert-map, obfuscate-string,
unstable-sort, opacity-to-hex, format-precision, format-number
and append-class.

*  Add tests for remaining untested helpers in data ns

Cover percent?, parse-percent, num-string?, num?, not-empty?,
editable-collection?, oreorder-before, oassoc-in-before,
lazy-map and reorder.

Platform-specific assertions use reader conditionals where
CLJS and JVM behaviour differ (js/isFinite string coercion,
js/isNaN empty-string coercion).
2026-03-24 19:52:52 +01:00
Andrey Antukh 7461c5304c Add comprehensive tests for app.common.colors ns (#8758)
Cover all public functions: valid-hex-color?, parse-rgb,
valid-rgb-color?, rgb->str, hex->rgb, rgb->hex, rgb->hsv,
hsv->rgb, rgb->hsl, hsl->rgb, hex->hsl, hex->hsv, hex->rgba,
hex->hsla, hex->lum, hsl->hex, hsl->hsv, hsv->hex, hsv->hsl,
format-hsla, format-rgba, expand-hex, prepend-hash, remove-hash,
color-string?, parse, next-rgb, reduce-range, interpolate-color,
uniform-spread, uniform-spread? and interpolate-gradient.

Tests pass on both JVM and JS (ClojureScript) platforms.
Platform differences (NaN saturation for achromatic colors,
integer vs float return types) are handled with mth/close?.
2026-03-24 19:10:44 +01:00
Andrey Antukh 0f19bc02d7 📎 Add testing engineer agent (opencode) 2026-03-24 18:49:30 +01:00
Andrey Antukh edfa437ce7 📚 Improve CONTRIBUTING.md file 2026-03-24 18:18:38 +01:00
Andrey Antukh cc03f3f884 📚 Add minor improvements to ai agents documentation 2026-03-24 18:00:39 +01:00
Andrey Antukh 750e8a9d51 🐛 Fix dissoc error when detaching stroke color from library (#8738)
* 🐛 Fix dissoc error when detaching stroke color from library

The detach-value function in color-row was only passing index to
on-detach, but the stroke's on-color-detach handler expects both
index and color arguments. This caused a protocol error when trying
to dissoc from a number instead of a map.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Fix crash when detaching color asset from stroke

The color_row detach-value callback calls on-detach with (index, color),
but stroke_row's local on-color-detach wrapper only took a single argument
(fn [color] ...), so it received index as color and passed it to
stroke.cljs which then called (dissoc index :ref-id :ref-file), crashing
with 'No protocol method IMap.-dissoc defined for type number'.

Fix the wrapper to accept (fn [_ color] ...) so it correctly ignores the
index passed by color_row (it already has index in the closure) and
forwards the actual color map to the parent handler.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 15:35:32 +01:00
Alejandro Alonso f88e287357 Merge pull request #8726 from penpot/niwinz-main-bugfix-1
🐛 Fix null text crash on paste in text editor
2026-03-24 15:19:41 +01:00
Andrey Antukh 56f1fcdb53 🐛 Fix crash when pasting image into text editor
When pasting an image (with no text content) into the text editor,
Draft.js calls handlePastedText with null/empty text. The previous fix
guarded splitTextIntoTextBlocks against null, but insertText still
attempted to build a fragment from an empty block array, causing
Modifier.replaceWithFragment to crash with 'Cannot read properties of
undefined (reading getLength)'.

Fix insertText to return the original state unchanged when there are no
text blocks to insert. Also guard handle-pasted-text in the ClojureScript
editor to skip the insert-text call entirely when text is nil or empty.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 13:00:28 +00:00
Andrey Antukh d863c7065f 🐛 Fix null text crash on paste in text editor
The splitTextIntoTextBlocks function in @penpot/draft-js called
.split() on the text parameter without a null check. When pasting
content without text data (e.g., images only), Draft.js passes null
to handlePastedText, causing a TypeError.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 13:49:28 +01:00
Andrey Antukh 8729fed724 📎 Add opencode and copilot deps on root package.json 2026-03-24 12:52:56 +01:00
Alejandro Alonso 5d6eb3b3d6 Merge pull request #8739 from penpot/niwinz-main-bugfix-5
🐛 Fix error when get-parent-with-data encounters non-Element nodes
2026-03-24 12:50:48 +01:00
Alejandro Alonso 3abd63c35a Merge pull request #8740 from penpot/niwinz-main-bugfix-7
🐛 Ensure path content is always PathData when saving
2026-03-24 12:44:51 +01:00
Andrey Antukh 13b5c96a42 📎 Update changelog 2026-03-24 09:19:58 +01:00
Andrey Antukh d051a3ba45 🐛 Ensure path content is always PathData when saving
The save-path-content function only converted content to PathData when
there was a trailing :move-to command. When there was no trailing
:move-to, the content from get-path was stored as-is, which could be
a plain vector if the shape was already a :path type with non-PathData
content. This caused segment/get-points to fail with 'can't access
property "get", cache is undefined' when the with-cache macro tried
to access the cache field on a non-PathData object.

The fix ensures content is always converted to PathData via path/content
before being stored in the state.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 08:15:58 +01:00
Andrey Antukh 577f00dd24 🐛 Fix error when get-parent-with-data encounters non-Element nodes
The get-parent-with-data function traverses the DOM using parentElement
to find an ancestor with a specific data-* attribute. When the current
node is a non-Element DOM node (e.g. Document node reached from event
handlers on window), accessing .-dataset returns undefined, causing
obj/in? to throw "right-hand side of 'in' should be an object".

This adds a nodeType check to skip non-Element nodes during traversal
and continue up the parent chain.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-23 19:19:27 +00:00
Andrey Antukh b484415a9f 🐛 Fix generic error shown on clipboard permission denial (#8666)
When the browser denies clipboard read permission (NotAllowedError),
the unhandled exception handler was showing a generic 'Something wrong
has happened' toast. This change adds proper error handling for
clipboard permission errors in paste operations and shows a
user-friendly warning message instead.

Changes:
- Add error handling in paste-from-clipboard for NotAllowedError
- Improve error handling in paste-selected-props to detect permission errors
- Mark clipboard NotAllowedError as ignorable in the uncaught error handler
  to prevent duplicate generic error toasts
- Add translation key for clipboard permission denied message

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-23 20:03:14 +01:00
Andrey Antukh 2d616cf9c0 📚 Add better organization for AGENTS.md file (#8675) 2026-03-18 14:59:38 +01:00
Andrey Antukh 1a59017e1c 🐛 Ignore posthog exceptions in unhandled exception handler (#8629)
PostHog recorder throws errors like 'Cannot assign to read only property
'assert' of object' which are unrelated to the application and should be
ignored to prevent noise in error reporting.
2026-03-17 18:41:06 +01:00
Elena Torró c6f8356847 📚 Improve docs on feature flags (#8641) 2026-03-17 11:49:14 +01:00
Andrey Antukh e730e9ee64 🐛 Fix subscribe to undefined stream error in use-stream hook (#8633)
Add a nil guard before subscribing to the stream in the use-stream
hook. When a nil/undefined stream is passed (e.g., from a conditional
expression or timing edge case during React rendering), the subscribe
call on undefined causes a TypeError. The guard ensures we only
subscribe when the stream is defined.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-17 10:06:16 +01:00
Andrey Antukh 0779c9ca61 🐛 Fix TypeError in get-points when content is not PathData (#8634)
The with-cache macro in impl.cljc assumed the target was always a
PathData instance (which has a cache field). When content was a plain
vector, (.-cache content) returned undefined in JS, causing:

  TypeError: Cannot read properties of undefined (reading 'get')

Fix:
- path/get-points (app.common.types.path) is now the canonical safe
  entry point: converts non-PathData content via impl/path-data and
  handles nil safely before delegating to segment/get-points
- segment/get-points remains a low-level function that expects a
  PathData instance (no defensive logic at that level)
- streams.cljs: replace direct call to path.segm/get-points with
  path/get-points so the safe conversion path is always used
- with-cache macro: guards against nil/undefined cache, falling back
  to direct evaluation for non-PathData targets

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-17 09:31:10 +01:00
andrés gonzález efd6d19a12 📚 Remove link to sales form from the Help Center (#8643) 2026-03-16 16:58:08 +01:00
Elena Torró ac69f28a0a Merge pull request #8646 from penpot/elenatorro-include-wasm-share-on-gitignore
🔧 Ignore render_wasm shared.js autogenerated file
2026-03-16 13:04:25 +01:00
Elena Torro ff1ba6b953 🔧 Ignore render_wasm shared.js autogenerated file 2026-03-16 12:09:54 +01:00
Andrey Antukh a528508751 📚 Update AGENTS.md 2026-03-13 12:57:22 +01:00
Andrey Antukh 5d931d1614 Merge remote-tracking branch 'origin/main' into staging 2026-03-13 09:20:30 +01:00
Andrey Antukh 33c5f82c43 🐛 Fix penpot.openPage() to navigate in same tab by default
- Change the default for the newWindow param from true to false, so
  openPage() navigates in the same tab instead of opening a new one
- Accept a UUID string as the page argument in addition to a Page object,
  avoiding the need to call penpot.getPage(uuid) first
- Add validation error when an invalid page argument is passed

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-13 09:19:50 +01:00
Alejandro Alonso ebd17974a6 Merge pull request #8580 from penpot/niwinz-staging-fix-paste-issue
🐛 Fix crash when pasting non-map transit clipboard data
2026-03-13 08:33:19 +01:00
Andrey Antukh eecb51ecc1 🐛 Fix clipboard getType error when no allowed types found (#8609)
When clipboard items have types that don't match the allowed types
list, the filtering results in an empty array. Calling getType with
undefined throws a NotFoundError. This change adds a check for null/undefined
types and filters them from the result.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-13 08:24:36 +01:00
Andrey Antukh e7e6303184 🐛 Make ct/format-inst nil safe (#8612)
Prevent JS TypeError when date is nil in date formatting.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 19:55:51 +01:00
Andrey Antukh 25df9f2f83 🐛 Fix DataCloneError in plugin postMessage communication
Fixes a crash where plugins sending messages via 'penpot.ui.sendMessage()'
could fail if their message payload contained non-serializable values like
functions or closures.

The fix adds validation using 'structuredClone()' to catch these messages
early with a helpful error message, and adds a defensive try/catch in the
modal's message handler as a safety net.

Fixes the error: 'Failed to execute postMessage on Window: ... could not
be cloned.'

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 15:26:00 +01:00
Andrey Antukh 8d5450391e 🐛 Fix crash when pasting non-map transit clipboard data
Guard against transit-decoded clipboard content that is not a map
before calling assoc, which caused a runtime crash ('No protocol
method IAssociative.-assoc defined for type number').

Also route :copied-props paste data to paste-transit-props instead
of incorrectly sending it to paste-transit-shapes.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 15:05:22 +01:00
Alejandro Alonso be9b1158ed Merge pull request #8588 from penpot/niwinz-staging-abort-signal-fix
🐛 Fix unhandled AbortError in HTTP fetch requests
2026-03-12 13:53:18 +01:00
Eva Marco 8f5c38d476 🐛 Fix scroll on colorpicker (#8595) 2026-03-12 13:36:38 +01:00
Andrey Antukh 80d165ed5b 🐛 Fix unhandled AbortError in HTTP fetch requests
Identify and silence "signal is aborted without reason" errors by:
- Providing an explicit reason to AbortController when subscriptions are disposed.
- Updating the global error handler to ignore AbortError exceptions.
- Ensuring unhandled rejections use the ignorable exception filter.

The root cause was RxJS disposal calling .abort() without a reason, combined
with the on-unhandled-rejection handler missing the ignorable error filter.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 13:31:58 +01:00
Alejandro Alonso 4b330e7b50 Merge pull request #8596 from penpot/niwinz-staging-fix-max-recursion
🐛 Fix RangeError (stack overflow) in find-component-main
2026-03-12 13:30:16 +01:00
Alejandro Alonso f5cabac5f3 Merge pull request #8583 from penpot/niwinz-staging-ignore-extensions-exceptions
🐛 Ignore browser extension errors in unhandled exception handler
2026-03-12 13:20:11 +01:00
Alejandro Alonso 1487386fbb Merge pull request #8582 from penpot/niwinz-staging-bugfix-path-plain-content
🐛 Fix plain vector leaking into shape :content from shape-to-path
2026-03-12 13:15:14 +01:00
Andrey Antukh b68e400cc1 🐛 Fix crash in select* when options vector is empty (#8578)
Guard get-option fallback with (when (seq options) ...) to avoid
"No item 0 in vector of length 0" when options is an empty vector.
Also guard the selected-option memo in select* to mirror the same
pattern already present in combobox*.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 13:06:25 +01:00
Alejandro Alonso 50c27aecc7 Merge pull request #8574 from penpot/niwinz-staging-unmount-fixes
🐛 Fix removeChild crash on portal-on-document* unmount
2026-03-12 13:06:00 +01:00
Alejandro Alonso c254f88367 Merge pull request #8575 from penpot/niwinz-staging-fetch-exception
🐛 Wrap fetch TypeError into proper ex-info with :unable-to-fetch code
2026-03-12 12:48:48 +01:00
Alejandro Alonso 8f7b12dfd8 Merge pull request #8569 from penpot/niwinz-staging-bugfix-2-not-iseqable-exception
🐛 Fix 'not ISeqable' error when entering float values in layout/opacity inputs
2026-03-12 12:37:35 +01:00
Andrey Antukh 82e3a5fa53 🐛 Fix 'not ISeqable' error when entering float values in layout/opacity inputs
Replace int? with number? in on-change handlers for layout item margins,
min/max sizes, and layer opacity. Using int? caused float values like 8.5
to fall into the design token branch, calling (first 8.5) and crashing.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 12:37:22 +01:00
Alejandro Alonso 1680be33ef Merge pull request #8568 from penpot/niwinz-staging-bugfix-1-path-get-points
🐛 Fix TypeError when path content is nil in get-points calls
2026-03-12 12:31:58 +01:00
Andrey Antukh 6ee8184821 🐛 Fix error when creating guides without frame (#8598)
* 🐛 Fix error when creating guides without frame

The error 'Cannot read properties of undefined (reading
$cljs$core$IFn$_invoke$arity$0$)' occurred when creating a new
guide. It is probably a race condition because it is not reproducible
from the user point of view.

The cause is mainly because of use incorrect jsx handler :& where :>
should be used. This caused that some props pased with incorrect casing
and the relevant callback props received as nil on the component and
on the use-guide hook.

The fix is simple: use correct jsx handler

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 💄 Add cosmetic changes to viewport guides components

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-12 12:23:09 +01:00
Eva Marco c00ef7c128 🐛 Fix unnexpected warning (#8603) 2026-03-12 12:01:28 +01:00
Xaviju 9b3207b06c 🐛 Fix wrong value on property copy on inspect styles (#8605) 2026-03-12 11:45:16 +01:00
Andrey Antukh 11a1ac2a09 🐛 Fix RangeError (stack overflow) in find-component-main
Refactor find-component-main to use an iterative loop/recur pattern instead of direct recursion and added cycle detection for malformed data structures.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-11 16:15:47 +01:00
Alonso Torres 31a4a7f21f 💄 Removed forgotten print (#8594) 2026-03-11 16:04:33 +01:00
Andrey Antukh 7ec9261475 Add improvements to AGENTS.md (#8586) 2026-03-11 15:24:40 +01:00
Andrey Antukh 33da7f384a Merge remote-tracking branch 'origin/staging' 2026-03-11 12:04:28 +01:00
David Barragán Merino e6d15a5ac2 🔧 Disable search indexing of plugin docs for non-production envs 2026-03-10 19:36:52 +01:00
David Barragán Merino e855907b05 🔧 Disable search indexing of plugin docs for non-production envs 2026-03-10 19:36:28 +01:00
Andrey Antukh db9e9f4832 🐛 Ignore browser extension errors in unhandled exception handler
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 19:30:01 +01:00
Andrey Antukh 7939cb045b 🐛 Fix plain vector leaking into shape :content from shape-to-path conversions
group-to-path was storing a raw concatenated vector into :content after
flattening children's PathData instances via (map vec). bool-to-path
was storing the plain-vector result of bool/calculate-content directly.
Both now wrap through path.impl/path-data at the assignment site so the
:content invariant (always a PathData instance) is upheld.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 19:26:03 +01:00
Andrey Antukh f566a2adfd 🐛 Fix ITransformable error when path content is a plain vector
Coerce content to PathData in transform-content before dispatching
the ITransformable protocol, so shapes carrying a plain vector in
their :content field (legacy data, bool shapes, SVG imports) no
longer crash with 'No protocol method ITransformable.-transform
defined for type object'.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 18:06:44 +00:00
Andrey Antukh 31d8b35a2c 📎 Revert small changes related to browser pool on exporter 2026-03-10 18:51:04 +01:00
Andrey Antukh fed01fba73 🐛 Wrap fetch TypeError into proper ex-info with :unable-to-fetch code
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 15:59:14 +01:00
Andrey Antukh 4f0bceddae 🐛 Fix stale deferred DOM ops in dashboard navigation
Two related issues that could cause crashes during fast navigation
in the dashboard:

1. grid.cljs: On drag-start, a temporary counter element is appended
   to the file card node for the drag ghost image, then scheduled for
   removal via requestAnimationFrame. If the user navigates away before
   the RAF fires, React unmounts the section and removes the card node
   from the DOM. When the RAF fires, item-el.removeChild(counter-el)
   throws because counter-el is no longer a child. Fixed by guarding
   the removal with dom/child?.

2. sidebar.cljs: Keyboard navigation handlers used ts/schedule-on-idle
   (requestIdleCallback with a 30s timeout) to focus the newly rendered
   section title after navigation. This left a very wide window for the
   callback to fire against a stale DOM after a subsequent navigation.
   Additionally, the idle callbacks were incorrectly passed as arguments
   to st/emit! (which ignores non-event values), making the scheduling
   an accidental side effect. Fixed by replacing all occurrences with
   ts/schedule (setTimeout 0), which is sufficient to defer past the
   current render cycle, and moving the calls outside st/emit!.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 14:22:45 +00:00
Andrey Antukh 80b64c440c 🐛 Fix removeChild crash on portal-on-document* unmount
The previous implementation passed document.body directly as the
React portal containerInfo. During unmount, React's commit phase
(commitUnmountFiberChildrenRecursively, case 4) sets the current
container to containerInfo and then calls container.removeChild()
for every DOM node inside the portal tree.

When two concurrent state updates are processed — e.g. navigating
away from a dashboard section while a file-menu portal is open —
React could attempt document.body.removeChild(node) twice for the
same node, the second time throwing:

  NotFoundError: Failed to execute 'removeChild' on 'Node':
  The node to be removed is not a child of this node.

The fix allocates a dedicated <div> container per portal instance
via mf/use-memo. The container is appended to body on mount and
removed in the effect cleanup. React then owns an exclusive
containerInfo and its unmount path never races with another
portal or the modal container (which also targets document.body).

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 14:22:34 +00:00
Andrey Antukh 98c1503bca Backport serveral plugin types documentation 2026-03-10 15:05:08 +01:00
Andrey Antukh 9f66220caa 🐛 Fix flex layout container horizontalSizing/verticalSizing via plugin API (#8555)
Setting horizontalSizing/verticalSizing on a FlexLayoutProxy was
dispatching update-layout-child instead of update-layout, so the
frame's auto-sizing (hug content) was never triggered even though
the getter read back the value correctly.

Also restricts accepted values to #{:fix :auto} (matching shape.cljs)
since frames cannot use :fill, and fixes a copy-paste error that
reported :horizontalPadding instead of :horizontalSizing in error messages.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 15:01:23 +01:00
Aitor Moreno 3112b0d8cf 🐛 Fix grow options not verifying text-editor/v2 (#8571) 2026-03-10 15:01:23 +01:00
Andrey Antukh ab90500ec8 🐛 Fix download-image to properly handle network errors and non-2xx responses (#8554)
The download-image function in app.media silently succeeded when the
remote image URL was unreachable or returned an error status code,
causing create-file-media-object-from-url to report success with no
actual image stored.

Add exception handling for connection refused, timeouts, and I/O errors
around the HTTP request, and validate the HTTP status code in
parse-and-validate before processing the response body.

Fixes #8499

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 15:01:23 +01:00
Andrey Antukh 0f47c30349 Merge branch 'main' into staging 2026-03-10 14:39:16 +01:00
Andrey Antukh 68fbacf8b3 Merge tag '2.14.0-RC2' 2026-03-10 14:38:58 +01:00
Andrey Antukh 7ab5f241da 🐛 Fix TypeError when path content is nil in get-points calls
Use nil-safe path/get-points wrapper (some-> based) instead of
direct path.segment/get-points calls in edition.cljs to prevent
'Cannot read properties of undefined (reading get)' crash.

Add nil-safety test to verify path/get-points returns nil without
throwing when content is nil.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-10 12:21:13 +00:00
Andrey Antukh 32cf95265a 📚 Add GitHub Copilot instructions (#8548) 2026-03-10 13:12:15 +01:00
Pablo Alba 34d29328e6 🐛 Fix bad size on switching a layout with fixed sizing (#8504) 2026-03-09 12:12:03 +01:00
Eva Marco c59cc4dff4 🐛 Fix tooltip position on absolute positioned elements (#8509)
* 🐛 Fix tooltip position on absolute positioned elements

* 🐛 Fix tests
2026-03-09 12:11:39 +01:00
Andrey Antukh 0ceadada35 🐛 Fix invalid data on layout flex dir shape property 2026-03-09 10:09:07 +01:00
Andrey Antukh 77955d7f91 Add several redundant checks for library-id on file rpc methods 2026-03-09 10:01:29 +01:00
Andrey Antukh 151238e518 💄 Add cosmetic change to link-file-to-library rpc method impl 2026-03-09 10:01:29 +01:00
Andrey Antukh 591d63e470 Add better error report on wrong input on logging helpers 2026-03-09 10:01:09 +01:00
andrés gonzález 5a6be141fd 📚 Add info about using math in tokens (#8510) 2026-03-04 14:59:04 +01:00
Eva Marco cc3033735b 🐛 Fix showing warning when no shape is selected (#8515) 2026-03-04 10:58:36 +01:00
Xaviju e1d556f4aa 🐛 Sort tokens by name (#8488) 2026-03-04 10:33:29 +01:00
Andrey Antukh c3f5117757 🐛 Fix unhandled exception on using decimals on stroke row (#8405) 2026-03-04 09:47:14 +01:00
Andrey AntukhandEva Marco 86e851f408 🐛 Fix incorrect version visibility on workspace (#8463)
* 🐛 Add missing order by clause to snapshot query

This fixes the incorrect snapshot visibility when file
has a lot of versions.

*  Reduce allocation on milestone-group* component

* 🐛 Fix milestone group timestamp formatting

* 📎 Update changelog

* 🐛 Fix scroll on history panel

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
2026-03-04 09:27:51 +01:00
Andrey Antukh a4351d133b Add minor improvements to error reporting (#8402) 2026-03-04 09:12:19 +01:00
Andrey Antukh b704a7da0e 🐛 Fix inconsistency between plugins api doc and impl for shadows (#8454)
Related to offset-x and offset-y attributes.
2026-03-04 09:09:27 +01:00
Andrey Antukh 478f631df5 🐛 Don't throw exception when picker is closed and image is still uploading (#8453)
*  Add notification tag to media uploading

This avoid hidding error messages once the upload
is finished.

* 🐛 Don't throw exception when picker is closed and image is still uploading
2026-03-04 09:07:15 +01:00
Andrey Antukh 57b9efbcd7 🐛 Fix redo operation on commenting on workspace (#8455) 2026-03-03 09:50:23 +01:00
andrés gonzález db0a8b65ca 📚 Add info about tokens remapping (#8503) 2026-03-03 09:02:31 +01:00
andrés gonzález 7c326e05e4 📚 Fix spanish text at docs (#8502) 2026-03-03 09:02:08 +01:00
andrés gonzález 58e86a545a 📚 Add info about grouping tokens (#8508) 2026-03-03 09:01:48 +01:00
Andrés Moya 31478c6afc 🐛 Fix validation of shadow token with missing keys (#8507) 2026-03-02 16:17:12 +01:00
Juan de la Cruz 7066afa01a 🎉 Add new slides 2.14 content (#8478) 2026-02-26 12:19:15 +01:00
Eva Marco 9345902a62 🐛 Fix cannot apply second token after creation while shape is selected (#8476) 2026-02-26 10:53:25 +01:00
Alonso Torres a4190df073 🐛 Fix problem with flex.appendChild with naturalOrdering on plugins API (#8470) 2026-02-26 10:47:44 +01:00
Eva Marco 47dae090ed 🐛 Add notification to token applied during text edition (#8434) 2026-02-26 10:24:48 +01:00
Andrés Moya c72e9ee1a0 🐛 Convert token values for the plugins 2026-02-25 14:04:20 +01:00
Andrés Moya ba87ea1a44 🔧 Add tokenscript flag and more validations to token values 2026-02-25 14:04:20 +01:00
Andrés Moya 72a855d4ac 🐛 Fix activeSets in themes API 2026-02-25 14:04:20 +01:00
Eva Marco e2377e8fa8 🐛 Fix input width on composite token form 2026-02-25 14:04:20 +01:00
Andrey Antukh b4c279ad7b 💄 Add minor cosmetic refactor on how plugin flags are stored
The main idea behind this, is move all plugin related stuff from
app.main.data.plugins into app.plugins.* and make them more consistent.
Also the intention that put all plugins related state under specific
prefix on the state.
2026-02-25 11:35:03 +01:00
Alonso Torres c972c06142 🐛 Fix problem with export dialog on single board (#8426) 2026-02-24 14:41:35 +01:00
Andrey Antukh d176da8012 Add jfr and jcmd to the backend docker image (#8446) 2026-02-24 00:08:14 +01:00
Andrey Antukh 20862c2da3 🐛 Fix incorrect plugin icon resolution 2026-02-24 00:07:30 +01:00
Andrey Antukh 1b8afccba2 Remove usage of multipart body size config on backend 2026-02-23 14:44:44 +01:00
Yamila Moreno dd856ecf50 ♻️ Deprecate PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE envvar 2026-02-23 13:48:01 +01:00
Andrey Antukh 145198c148 📎 Use proper version tag on frontend index template 2026-02-23 12:17:58 +01:00
alonso.torres eddfc4c4b2 🐛 Fix problem with createText in plugins 2026-02-23 09:35:30 +01:00
alonso.torres e6e34af391 🐛 Show outline on hidden paths 2026-02-23 09:34:50 +01:00
Andrés Moya 3d41dc276e 🐛 Fix resolve tokens with tokenscript when type is font family 2026-02-20 12:41:17 +01:00
alonso.torres cee974a906 🐛 Fix problem with tokens in plugins 2026-02-18 17:20:46 +01:00
Alejandro Alonso a1a7f643ec Merge pull request #8390 from penpot/niwinz-staging-exporter-bundle
🐛 Fix exporter bundle deps issue with pnpm
2026-02-17 18:38:50 +01:00
Andrey Antukh 70013fde74 🐛 Fix exporter bundle deps issue with pnpm 2026-02-17 17:46:09 +01:00
Andrey Antukh 916107ce04 📎 Update pnpm-lock.yaml file on plugins module 2026-02-17 17:42:39 +01:00
Andrey Antukh 8eb5bd3dd8 🔧 Add minor adjustments to mcp build-types script 2026-02-17 17:42:39 +01:00
Andrey Antukh 5718698bff Update mcp api_types.yml file with latest plugins doc updates 2026-02-17 17:42:39 +01:00
Andrey AntukhandLuis de Dios f00b222262 Revert "♻️ Replace some components with DS ones" (#8384)
* Revert "♻️ Replace some components with DS ones"

This reverts commit 6879f54e5d.

* 📎 Restore missing styles

* 📎 Fix tests

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-02-17 16:23:04 +01:00
Andrey Antukh 137febcbab 📎 Clean changelog 2026-02-17 10:00:42 +01:00
Andrey Antukh f98c0bbd16 📎 Update changelog 2026-02-17 09:58:40 +01:00
Luis de Dios 7f3212d5a4 🐛 Fix changelog to remove MCP (#8380) 2026-02-17 09:53:12 +01:00
Andrés Moya 643cd6f61f 🐛 Add resolved value to tokens in plugins API (#8372) 2026-02-17 09:20:04 +01:00
Luis de Dios 166dc05ff2 🐛 Fix incorrect icons in grid view (#8373) 2026-02-16 16:39:39 +01:00
Andrés Moya 619e2387dc 🐛 Fix applied tokens property names 2026-02-16 15:16:14 +01:00
Andrés Moya 813c804d45 🔧 Enhance schema validation of token application 2026-02-16 15:16:14 +01:00
Andrey Antukh 63f0c68977 Merge remote-tracking branch 'origin/main' into staging 2026-02-16 14:35:28 +01:00
Andrey Antukh 1f2a234458 📚 Update changelog 2026-02-16 14:32:28 +01:00
Andrey Antukh b281870c50 📚 Update changelog 2026-02-16 14:27:33 +01:00
Andrey Antukh 3909bc0fc1 Merge remote-tracking branch 'origin/main' into staging 2026-02-16 14:17:46 +01:00
Andrey Antukh b6427ecaac 🐛 Revert yetti upgrade
Because of regression introduced on undertow-core 2.3.19
2026-02-16 14:16:29 +01:00
Andrey Antukh e82319c49e Merge tag '2.13.2' 2026-02-16 14:13:51 +01:00
Andrey Antukh ce63bae92d Add better approach for error handling to obj/reify 2026-02-16 11:07:40 +01:00
Andrey Antukh d1d50138ed Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-16 10:00:46 +01:00
Andrey Antukh c63de58b7f Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-16 10:00:27 +01:00
Andrey Antukh 7301973655 🌐 Rehash and sync translation files 2026-02-16 09:41:49 +01:00
Sebastiaan Pasma e6990e996c 🌐 Add translations for: Dutch
Currently translated at 98.9% (2052 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-02-16 09:35:25 +01:00
Alejandro Alonso 82a6e73b0d 🌐 Add translations for: Yoruba
Currently translated at 56.5% (1172 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2026-02-16 09:35:24 +01:00
VKing9 a0fc14d4e8 🌐 Add translations for: Hindi
Currently translated at 96.3% (1998 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-02-16 09:35:24 +01:00
Anonymous a5dc3a4e2b 🌐 Add translations for: Croatian
Currently translated at 76.9% (1596 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-02-16 09:35:24 +01:00
Ņikita K. f60f259937 🌐 Add translations for: Latvian
Currently translated at 90.7% (1883 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-02-16 09:35:23 +01:00
Anonymous c669bd7327 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 86.8% (1801 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-02-16 09:35:23 +01:00
Anonymous c33a52ec51 🌐 Add translations for: Basque
Currently translated at 55.5% (1152 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2026-02-16 09:35:23 +01:00
Anonymous 9b2e847d99 🌐 Add translations for: Spanish
Currently translated at 97.1% (2015 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-02-16 09:35:22 +01:00
Anonymous d1e82c4d40 🌐 Add translations for: Hebrew
Currently translated at 95.6% (1984 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-02-16 09:35:22 +01:00
AlexTECPlayz 71ef2294e4 🌐 Add translations for: Romanian
Currently translated at 93.3% (1937 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-02-16 09:35:21 +01:00
Alejandro Alonso 3576c581a3 🌐 Add translations for: Arabic
Currently translated at 54.1% (1123 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-02-16 09:35:21 +01:00
Anonymous 13be3599b6 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 75.6% (1568 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-02-16 09:35:20 +01:00
Nicola Bortoletto 7b5e1f0196 🌐 Add translations for: Italian
Currently translated at 98.6% (2047 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-02-16 09:35:20 +01:00
Anonymous a483c99480 🌐 Add translations for: Polish
Currently translated at 54.3% (1127 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2026-02-16 09:35:20 +01:00
Radek Sawicki 60ebbd57c6 🌐 Add translations for: Polish
Currently translated at 54.3% (1127 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2026-02-16 09:35:19 +01:00
Црнобог e830689987 🌐 Add translations for: Serbian
Currently translated at 65.9% (1368 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2026-02-16 09:35:19 +01:00
Amerey.eu c8f9f49793 🌐 Add translations for: Czech
Currently translated at 76.7% (1591 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2026-02-16 09:35:19 +01:00
Anonymous f24f872c24 🌐 Add translations for: French
Currently translated at 95.2% (1976 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-02-16 09:35:19 +01:00
Renan Mayrinck 4a1a7e6e8f 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 67.0% (1391 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-02-16 09:35:18 +01:00
Denys Kisil 46ee8402fe 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 87.5% (1815 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-02-16 09:35:18 +01:00
The_BadUser b6d813dd51 🌐 Add translations for: Russian
Currently translated at 76.1% (1579 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-02-16 09:35:13 +01:00
Anonymous a9f674389a 🌐 Add translations for: German
Currently translated at 94.4% (1958 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-02-16 09:35:13 +01:00
william chen 78bdb13831 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 77.0% (1599 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-02-16 09:35:12 +01:00
im424 e548b2d863 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 77.0% (1599 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-02-16 09:35:12 +01:00
Alejandro Alonso 7543527fae 🌐 Add translations for: Hausa
Currently translated at 59.6% (1238 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2026-02-16 09:35:12 +01:00
Anonymous da488373e2 🌐 Add translations for: Turkish
Currently translated at 98.9% (2052 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-02-16 09:35:12 +01:00
Henrik Allberg 91e1c8299d 🌐 Add translations for: Swedish
Currently translated at 95.6% (1983 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-02-16 09:35:11 +01:00
Linerly adc661126a 🌐 Add translations for: Indonesian
Currently translated at 81.6% (1694 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2026-02-16 09:35:11 +01:00
Hosted Weblate 3b07ff2613 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-02-16 09:15:59 +01:00
Alexis Morin 3aebff0799 🌐 Add translations for: French (Canada)
Currently translated at 61.2% (1266 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin 3b14acc93e 🌐 Add translations for: French (Canada)
Currently translated at 60.2% (1244 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin ad2db07123 🌐 Add translations for: French (Canada)
Currently translated at 55.8% (1154 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin 068ecc3847 🌐 Add translations for: French (Canada)
Currently translated at 55.4% (1146 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin c850fe2d4f 🌐 Add translations for: French (Canada)
Currently translated at 55.0% (1138 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin 59acf71255 🌐 Add translations for: French (Canada)
Currently translated at 52.4% (1083 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin d7c5e7798c 🌐 Add translations for: French (Canada)
Currently translated at 48.8% (1009 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin ee81459d87 🌐 Add translations for: French (Canada)
Currently translated at 44.6% (923 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin ea54ff4bd5 🌐 Add translations for: French (Canada)
Currently translated at 43.6% (902 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin 353998b989 🌐 Add translations for: French (Canada)
Currently translated at 42.3% (874 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin 6b1ae46116 🌐 Add translations for: French (Canada)
Currently translated at 40.8% (844 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Sandy S Kuo 727a0adf1f 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 77.5% (1603 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-02-16 09:15:49 +01:00
Alexis Morin b99e4e8954 🌐 Add translations for: French (Canada)
Currently translated at 37.5% (775 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Alexis Morin e8f893a668 🌐 Add translations for: French (Canada)
Currently translated at 36.6% (758 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Yaron Shahrabani 44964bc98b 🌐 Add translations for: Hebrew
Currently translated at 96.2% (1988 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-02-16 09:15:49 +01:00
Dogyeong 86f52a15a5 🌐 Add translations for: Korean
Currently translated at 12.1% (250 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-02-16 09:15:49 +01:00
Alexis Morin cc43426bb8 🌐 Add translations for: French (Canada)
Currently translated at 35.8% (740 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:49 +01:00
Dogyeong 5da2865158 🌐 Add translations for: Korean
Currently translated at 11.4% (237 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-02-16 09:15:49 +01:00
Alexis Morin eeb998409d 🌐 Add translations for: French (Canada)
Currently translated at 33.8% (699 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Alexis Morin b9ac8f7616 🌐 Add translations for: French (Canada)
Currently translated at 32.2% (667 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Nicola Bortoletto 626d6a285d 🌐 Add translations for: Italian
Currently translated at 99.5% (2057 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-02-16 09:15:48 +01:00
Alexis Morin 7159d00d75 🌐 Add translations for: French (Canada)
Currently translated at 29.3% (607 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Louis Chance 380c5382c7 🌐 Add translations for: French
Currently translated at 95.8% (1980 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-02-16 09:15:48 +01:00
Alexis Morin 28c244f8ee 🌐 Add translations for: French (Canada)
Currently translated at 24.1% (499 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Stas Haas af149afd08 🌐 Add translations for: German
Currently translated at 95.0% (1963 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-02-16 09:15:48 +01:00
Alexis Morin afd8add839 🌐 Add translations for: French (Canada)
Currently translated at 22.6% (467 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Andrey Antukh d6aad1d79b 🌐 Add translations for: French
Currently translated at 95.1% (1966 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-02-16 09:15:48 +01:00
Nicola Bortoletto 621e5161dc 🌐 Add translations for: Italian
Currently translated at 99.1% (2049 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-02-16 09:15:48 +01:00
Alexis Morin 00df74d602 🌐 Add translations for: French (Canada)
Currently translated at 21.7% (449 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Alexis Morin d31501ddb6 🌐 Add translations for: French (Canada)
Currently translated at 18.9% (392 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Louis Chance b0fda4cb06 🌐 Add translations for: French
Currently translated at 95.1% (1966 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-02-16 09:15:48 +01:00
Stephan Paternotte ce80c049cd 🌐 Add translations for: Dutch
Currently translated at 99.8% (2062 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-02-16 09:15:48 +01:00
Marius 2873bffff1 🌐 Add translations for: German
Currently translated at 93.7% (1937 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-02-16 09:15:48 +01:00
Alexis Morin 56c7ef8e99 🌐 Add translations for: French (Canada)
Currently translated at 18.7% (388 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:48 +01:00
Edgars Andersons 594551c16a 🌐 Add translations for: Latvian
Currently translated at 91.3% (1887 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-02-16 09:15:47 +01:00
Stephan Paternotte 00c34ecf12 🌐 Add translations for: Dutch
Currently translated at 99.8% (2062 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-02-16 09:15:47 +01:00
Alexis Morin 033a1f39fa 🌐 Add translations for: French (Canada)
Currently translated at 17.5% (363 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-02-16 09:15:47 +01:00
Oğuz Ersen aaab3e6b3e 🌐 Add translations for: Turkish
Currently translated at 99.8% (2062 of 2066 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-02-16 09:15:47 +01:00
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
Elena Torró 51782551cc Merge pull request #8346 from penpot/alotor-fix-problem-text-autogrouw
🐛 Fix problem with autogrow change while editing text
2026-02-13 15:25:35 +01:00
alonso.torres f60a4cd111 🐛 Fix problem with autogrow change while editing text 2026-02-13 14:52:40 +01:00
David Barragán Merino 1349789a7b 🔧 Fix the plugin style documentation build command 2026-02-13 14:20:34 +01:00
David Barragán Merino 8dbb169061 🔧 Fix the plugin style documentation build command 2026-02-13 14:20:01 +01:00
David Barragán Merino cc28bd44f6 🔧 Fix the plugin style documentation build command 2026-02-13 14:18:35 +01:00
Alejandro Alonso d9d4a99e1d 🔧 Migrate variants tests to wasm viewport 2026-02-13 14:13:03 +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
Elena Torró 38ad24ea07 Merge pull request #8349 from penpot/alotor-fix-editor-selrect
🐛 Fix problem with text editor outline
2026-02-13 13:25: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
Elena Torró 80a3f4cd60 Merge pull request #8350 from penpot/superalex-migrate-tests-to-wasm-viewport-3
🔧 Migrate inspect tab tests to wasm viewport
2026-02-13 13:06:14 +01:00
alonso.torres 3ea0a781f1 🐛 Fix problem when create click 2026-02-13 12:38:33 +01:00
alonso.torres 35abf8a179 🐛 Fix problem with text editor outline 2026-02-13 12:23:05 +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
Alejandro Alonso 7e9fb0742d 🔧 Migrate inspect tab tests to wasm viewport 2026-02-13 12:09:31 +01:00
Elena Torró 39b3767203 Merge pull request #8348 from penpot/superalex-fix-non-existent-google-font
🐛 Fix non existent google font
2026-02-13 12:08:18 +01:00
Alejandro Alonso 23333aa3c3 Merge pull request #8347 from penpot/elenatorro-13386-fix-flex-absolute-index
🐛 Fix absolute z-index values on layout children
2026-02-13 12:06:45 +01:00
Alejandro Alonso 684e2b6950 🐛 Fix non existent google font 2026-02-13 11:59:59 +01:00
Elena Torro fd6f70a740 🐛 Fix absolute z-index values on layout children 2026-02-13 11:41:16 +01:00
Elena Torro b892cc9b14 🔧 Refactor shape base props to use transmute 2026-02-13 11:28:18 +01:00
Belén Albeza 50ddf5e628 🔧 Add integration test for bug 13305 2026-02-13 10:57:27 +01:00
Belén Albeza 75a4102637 🐛 Fix resize board to fit (wasm) 2026-02-13 10:57:27 +01:00
David Barragán Merino d7203ef24c 🔧 Fix the plugin bundle build command 2026-02-13 09:39:12 +01:00
David Barragán Merino 61f3e090da 🔧 Fix the plugin bundle build command 2026-02-13 09:38:39 +01:00
David Barragán Merino fda09b02b9 🔧 Fix the plugin bundle build command 2026-02-13 09:37:22 +01:00
Elena Torró 8f478aa6e5 Merge pull request #8325 from penpot/superalex-fix-wasm-forcing-url-param
🐛 Fix forcing wasm via url param
2026-02-12 17:46:50 +01:00
Alejandro Alonso 95e1efa5ff 🐛 Fix forcing wasm via url param 2026-02-12 17:35:02 +01:00
Andrés Moya a23ca6a1cb 🐛 Fix applied tokens reading in shape proxy 2026-02-12 17:14:16 +01:00
Eva Marco d07f568ba2 🐛 Avoid modifying shape by apply negative tokens to border radius (#8336) 2026-02-12 17:01:36 +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
Florian Schroedl 375608b44b ⬆️ Update tokenscript interpreter to 0.26.0 and add CSS color schemas
Regenerate schemas.js with preset:cssColors to support CSS color constants.
2026-02-12 14:14:45 +01:00
Elena Torró 97d24b190f Merge pull request #8334 from penpot/superalex-migrate-tests-to-wasm-viewport
🔧 Migrate straightforward tests to user the wasm viewport
2026-02-12 14:03:06 +01:00
Alejandro Alonso 434ac0556a 🔧 Migrate straightforward tests to user the wasm viewport 2026-02-12 13:29:13 +01:00
Andrey Antukh 12e5d8d8c4 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-12 11:00:56 +01:00
Andrey Antukh 04a3126856 Merge remote-tracking branch 'origin/main' into staging-render 2026-02-12 11:00:38 +01:00
Elena Torró 2f71663470 Merge pull request #8245 from penpot/elenatorro-13047-setup-embedded-text-editor
🔧 Set up embedded editor
2026-02-12 10:05:39 +01:00
Andrey Antukh 43cb313cd7 Merge pull request #8310 from oraios/mcp-tokens
 MCP improvements to enable UC2, design token handling
2026-02-12 09:47:32 +01:00
Elena Torró 0b199c606a Merge pull request #8331 from penpot/ladybenko-add-wasm-config-playwright-helper
🔧 Add helper utils to mock config flags for WasmWorkspacePage (e2e)
2026-02-12 09:45:03 +01:00
Aitor Moreno 54f63c5dc5 ♻️ Refactor minor things 2026-02-12 09:34:21 +01:00
Elena Torro a14c36e996 📚 Add embedded text editor MVP documentation 2026-02-12 09:34:20 +01:00
Elena Torro 2b525f0f48 🔧 Set up embedded editor 2026-02-12 09:34:20 +01:00
Belén Albeza fd6ff04e90 🔧 Add helper utils to mock config flags for WasmWorkspacePage (e2e) 2026-02-12 09:25:08 +01:00
eps-epsiloneridani dbb0aa8ce2 📚 Update recommended-settings.md (#8330)
Got rid of a stray quotation mark

Signed-off-by: eps-epsiloneridani <162043859+eps-epsiloneridani@users.noreply.github.com>
2026-02-12 09:19:10 +01:00
Andrey Antukh 12822833f6 Merge pull request #8301 from eureka928/fix/4513-shift-arrow-color-inputs
🐛 Add Shift/Alt arrow key stepping to color picker inputs
2026-02-12 08:26:05 +01:00
eureka928 307ae374fe ♻️ Unify color picker input handlers by treating alpha as a property
Eliminate duplicated on-change-opacity and on-key-down-opacity handlers
by routing alpha through apply-property-change, and extract shared
stepping logic into on-key-down-step.

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-02-12 08:25:37 +01:00
eureka928 7d7dbd4662 🐛 Add Shift/Alt arrow key stepping to color picker inputs (#4513)
Color picker numeric inputs (R, G, B, H, S, V, Alpha) now support
Shift+Arrow for ×10 steps and Alt+Arrow for ×0.1 steps, matching
the behavior of numeric inputs elsewhere in the application.

Signed-off-by: eureka928 <meobius123@gmail.com>
2026-02-12 08:25:37 +01:00
Alejandro Alonso 139d4ba13c Merge pull request #8328 from penpot/elenatorro-13311-fix-multiple-strokes-blending
🐛 Fix stroke color aliasing when a shape has multiple strokes
2026-02-12 07:05:44 +01:00
Elena Torro 0cb5c16823 🐛 Fix fallback font 2026-02-12 06:43:52 +01:00
Elena Torro 4ed1a544f8 🐛 Fix stroke color aliasing when a shape has multiple strokes 2026-02-12 06:43:52 +01:00
Elena Torró 566ac67fc9 Merge pull request #8324 from penpot/azazeln28-fix-editor-fills
🐛 Fix text editor issues
2026-02-11 16:37:20 +01:00
Juanfran 394d597736 Add enhacements to plugins build mechanism (#8326)
* 🐛 Fix plugin code build

* 🔧 Update editor.defaultFormatter to new Prettier

* 🐛 Fix lint issues in create-palette-plugin

* 🐛 Add missing run in pnpm init script for plugins
2026-02-11 15:28:33 +01:00
Aitor Moreno b2231e520c 📚 Add best practices to text editor README.md 2026-02-11 13:09:56 +01:00
Aitor Moreno e722e17b10 🐛 Fix paragraph styles not being applied 2026-02-11 12:49:20 +01:00
Aitor Moreno 755d720b34 🐛 Fix text editor fills not being updated 2026-02-11 12:29:03 +01:00
Alejandro Alonso d991d59852 Merge pull request #8318 from penpot/elenatorro-13311-fix-multiple-fills-blending
🐛 Fix fill aliasing when a shape has multiple fills
2026-02-11 11:37:43 +01:00
Dominik Jain 7eb9a207f5 Change PenpotUtils.findShapes to search on all pages by default
This matches the behaviour of findShape, more closely aligning with
the LLM's expectations (given the lack of concrete information in
the instructions)
2026-02-11 11:35:10 +01:00
Dominik Jain 8ac17604fd Improve information on component instances
* Add information on detachment
* Add information on remove behaviour in component instances
2026-02-11 11:35:10 +01:00
Elena Torro eede023d6b 🐛 Fix fill aliasing when a shape has multiple fills 2026-02-11 11:21:08 +01:00
Belén Albeza ccd42852b7 🐛 Fix token not being highlighted (wasm) 2026-02-11 11:17:27 +01:00
Alejandro Alonso a2f7ae549e Merge pull request #8312 from penpot/elenatorro-13256-sync-text-selection
🔧 Hide text color from selected text
2026-02-11 11:02:35 +01:00
Alejandro Alonso 6f74d458a8 🐛 Adding lost file for render e2e testing get-file-stroke-styles.json 2026-02-11 10:47:50 +01:00
Alejandro Alonso 8d033de145 Merge pull request #8299 from penpot/elenatorro-13242-review-performance
🔧 Improve layout performance
2026-02-11 10:45:40 +01:00
Dominik Jain f5afcde0de Improve shapeStructure
* Add information on component instance (component id, name; main instance id)
* Improve JSON result order (children should come last)
2026-02-11 10:45:22 +01:00
Dominik Jain b6dfdc23cd Update information on TokenProperty 2026-02-11 10:45:22 +01:00
Dominik Jain a5a084cf0f Update API type information based on current repo state 2026-02-11 10:45:22 +01:00
Dominik Jain 1546025814 Avoid certain <ul> elements with single <li> generating bullets 2026-02-11 10:45:22 +01:00
Dominik Jain 8de510d1c6 🐛 Fix PenpotAPIDocsProcessor not requiring the url parameter
* Add additional constant for the PROD url
* Adapt the debug function to use a URL
* Improve logging
2026-02-11 10:45:10 +01:00
Elena Torró 2e77c09ca5 Merge pull request #8309 from penpot/superalex-fix-stroke-dot-dash-mix
🐛 Fix dot strokes
2026-02-11 10:37:46 +01:00
Elena Torró 47346e478e Merge pull request #8303 from penpot/superalex-fix-stroke-opacity-for-boards
🐛 Fix stroke opacity for boards
2026-02-11 10:05:47 +01:00
Andrey Antukh 89cd4d820c 🔥 Remove mcp from compose 2026-02-11 09:13:24 +01:00
Alejandro Alonso f32c377f17 🐛 Fix stroke opacity for boards 2026-02-11 09:08:03 +01:00
Andrey Antukh 8693623b13 📎 Update SECURITY.md file 2026-02-11 08:11:04 +01:00
Alejandro Alonso 97f01c646d 🎉 Improve multiple emoji E2E test 2026-02-11 07:36:22 +01:00
Alejandro Alonso eea1d3c0a5 🎉 Improve updating canvas background E2E test 2026-02-11 07:19:22 +01:00
Alejandro Alonso 9eef4de87d 🐛 Fix dot/dahs/mixed strokes 2026-02-11 07:08:28 +01:00
Andrey Antukh f4d07a3c36 ⬆️ Update pnpm on frontend and plugins modules 2026-02-10 19:02:32 +01:00
Francis Santiago 15fa6206e2 Merge pull request #8286 from penpot/fc-fix-ipv6-fronted
🎉 Enable IPv6 support for docker images
2026-02-10 16:01:06 +01:00
Francis Santiago 3281819283 🎉 Enable IPv6 support for docker images 2026-02-10 15:52:33 +01:00
Andrés MoyaandAndrey Antukh 41f29767db 🐛 Fix configuration of poc-tokens-plugin (#8314)
* 🐛 Fix configuration of poc-tokens-plugin

* 📎 Add missing changes on poc-tokens-pligin tsconfig

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-02-10 15:18:11 +01:00
Dominik Jain 76289df32c Establish compatibility with new member anchors (h3 instead of a tag) 2026-02-10 14:03:40 +01:00
Andrey Antukh 920fbe34ad 🐛 Fix invalid deps passed to http management routes service 2026-02-10 13:46:29 +01:00
Elena Torro 187d1118c0 🔧 Hide text color from selected text 2026-02-10 13:15:55 +01:00
Dominik Jain a674b5f914 📚 Add instructions on running only the docs server 2026-02-10 12:53:20 +01:00
Dominik Jain 71507fb9b7 ♻️ Adjust ConfigurationLoader to use markdown file instead of yml 2026-02-10 12:35:44 +01:00
Dominik Jain 024aedc3ca ♻️ Convert prompt content to markdown format 2026-02-10 12:35:44 +01:00
Dominik Jain 44657c95df ♻️ Rename prompts.yml -> initial_instructions.md 2026-02-10 12:35:44 +01:00
Dominik Jain d4d5009a3d Improve prompts on token application 2026-02-10 12:35:44 +01:00
Dominik Jain bb4d0322d8 🚧 Temporarily add ts-ignore statements
This shall be reverted once the new API types are published
2026-02-10 12:35:44 +01:00
Dominik Jain 56e369a1c0 Add helper functions for token exploration
Extend PenpotUtils with helper functions for token exploration/discovery
and describe them in the system prompt
2026-02-10 12:35:44 +01:00
Dominik Jain 6b277956b9 Add information on clone() method 2026-02-10 12:35:44 +01:00
Dominik Jain e9a56c9d9f Shorten design token instructions 2026-02-10 12:35:44 +01:00
Dominik Jain 8d90edcc2f Add instructions on design tokens 2026-02-10 12:35:44 +01:00
Dominik Jain 8186f3c87c 📚 Remove misleading information from README
The types build is not part of the bootstrap, and it is not
relevant to regular users (only to developers).

Information on how to apply it is now in types-generator/README.md
2026-02-10 12:35:44 +01:00
Dominik Jain d7282518c4 📚 Improve usage documentation of API type generator script 2026-02-10 12:35:44 +01:00
Dominik Jain 467eb3c333 Update API docs to include token-related types 2026-02-10 12:35:44 +01:00
Dominik Jain d2299f83ec Apply bash in build scripts explicitly (Win compatibility) 2026-02-10 12:35:44 +01:00
Andrey Antukh 11a283916d Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-10 11:58:27 +01:00
Andrey Antukh f08700945a Merge remote-tracking branch 'origin/staging' into develop 2026-02-10 11:58:09 +01:00
Andrey Antukh 59711a1cf8 📎 Update changelog 2026-02-10 11:57:01 +01:00
Aitor Moreno e9b2e9e818 🚑 Hot fix for text editor internal error 2026-02-10 11:10:16 +01:00
Belén Albeza c4aa51bc01 🐛 Fix permanent blur when switching pages 2026-02-10 10:59:47 +01:00
Belén Albeza 1c270ac9c6 Remove leftover println in render code 2026-02-10 10:59:47 +01:00
Andrey Antukh 06e5825c8a 🐛 Add proper input checking to font related RCP method 2026-02-10 10:36:57 +01:00
Andrey Antukh e3dfa69011 Make plugins devserver to be able run inside devenv 2026-02-10 08:29:24 +01:00
Juanfran 96b682aa12 ♻️ Remove Nx and rely on pnpm monorepo features 2026-02-10 08:29:24 +01:00
Juanfran 45d04942cc Add example ui storybook 2026-02-10 08:29:24 +01:00
Juanfran 07055b53d1 ⬆️ Update plugins dependencies 2026-02-10 08:29:24 +01:00
Andrey Antukh d30387eb77 Backport docker images changes from develop 2026-02-09 19:21:30 +01:00
Andrey Antukh 33fd672c21 Backport MCP related changes from develop (#8306) 2026-02-09 18:00:43 +01:00
Andrey Antukh dd7038bdad 📎 Fix fmt issue on frontend code 2026-02-09 17:38:40 +01:00
Andrey Antukh 5ec345162a Add mcp plugin into the frontend bundle 2026-02-09 17:38:40 +01:00
Andrey Antukh 0027e9031a Make mcp env vars to use the same convention as penpot 2026-02-09 17:38:40 +01:00
Pablo Alba 5d3ccbc8b4 Add managed profiles endpoint to nitrate api (#8292) 2026-02-09 15:52:18 +01:00
Andrés Moya 1a1c351466 🐛 Fix dependency 2026-02-09 15:06:39 +01:00
Andrés MoyaandAndrey Antukh 5b5f22a8c6 🎉 Add tokens to Penpot Plugins API (#7756)
* 🎉 Add tokens to plugins API documentation

And add poc plugin example

* 📚 Document better the tokens value in plugins API

* 🔧 Refactor token validation schemas

* 🔧 Use automatic validation in token proxies

* 🔧 Use schemas to validate token creation

* 🔧 Use multi schema for token value

* 🔧 Use schema in token api methods

* 🐛 Fix review comments

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-02-09 14:18:31 +01:00
Andrey Antukh ac1c3ff184 Merge branch 'staging-render' into develop 2026-02-09 14:14:02 +01:00
Andrey Antukh 43cd92c76d Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-09 14:12:55 +01:00
Elena Torró cf2b40a097 Merge pull request #8302 from penpot/azazeln28-issue-13124-text-not-restored-undoing
🐛 Fix text not restored on ctrl+z
2026-02-09 13:41:43 +01:00
Aitor Moreno b72959544c 🐛 Fix text not restored on ctrl+z 2026-02-09 13:29:31 +01:00
Andrey Antukh a7b2e98b8e ⬆️ Use latest imagemagick version on docker images 2026-02-09 13:19:26 +01:00
Andrey Antukh d979894872 Add libxml2 dep to imagemagick dockerfile 2026-02-09 12:27:44 +01:00
Alejandro Alonso 3d20fc508d 🐛 Fix image magick info call (#8300) 2026-02-09 12:26:42 +01:00
Elena Torro 969666b39b 🔧 Simplify view interaction log message
Remove zoom_changed from log output as it's no longer needed
for debugging after the tile optimization changes.
2026-02-09 11:44:50 +01:00
Yamila Moreno d953222eb4 🔧 Add CI for MCP server (#8293) 2026-02-09 11:25:24 +01:00
Elena Torró b3faa985ce Merge pull request #8291 from penpot/superalex-fix-dashboard-navigation
🐛 Fix dashboard navigation from workspace
2026-02-09 09:59:11 +01:00
Elena Torro a8322215dd 🔧 Optimize pan/zoom tile handling
- Add incremental tile update that preserves cache during pan
- Only invalidate tile cache when zoom changes
- Force visible tiles to render synchronously (no yielding)
- Increase interest area threshold from 2 to 3 tiles
2026-02-09 09:38:01 +01:00
Elena Torro e1ce97a2b4 🔧 Prioritize visible tiles over interest-area tiles
Partition pending tiles into 4 groups by visibility and cache status.
Visible tiles are processed first to eliminate empty squares during
pan/zoom. Cached tiles within each group are processed before uncached.
2026-02-09 09:38:01 +01:00
Elena Torro 2ccd2a6679 🔧 Use HashSet for grid layout children lookup
HashSet provides O(1) contains() vs Vec's O(n), improving
child lookup performance in grid cell data creation.
2026-02-09 09:38:01 +01:00
Elena Torro 2d9a2e0d50 🔧 Use swap_remove in flex layout distribution
swap_remove is O(1) vs remove's O(n) when order doesn't matter.
These loops iterate backwards, so swap_remove is safe.
2026-02-09 09:38:01 +01:00
Elena Torro 216d400262 🔧 Prevent duplicate layout calculations
Use HashSet for layout_reflows to avoid processing the same
layout multiple times. Also use std::mem::take instead of
creating a new Vec on each iteration.
2026-02-09 09:37:58 +01:00
Elena Torro c87ffdcd30 🔧 Add forward children iterator for flex layout
Avoid Vec allocation + reverse for reversed flex layouts.
The new children_ids_iter_forward returns children in original order,
eliminating the need to collect and reverse.
2026-02-09 09:35:04 +01:00
Elena Torro 8ef6600cdc 🔧 Return HashSet from update_shape_tiles
Avoid final collect() allocation by returning HashSet directly.
Callers already use extend() which works with both types.
2026-02-09 09:35:04 +01:00
Elena Torro a3764b9713 🔧 Avoid clone in rebuild_touched_tiles
Use std::mem::take instead of clone to avoid HashSet allocation.
The set was cleared anyway by clean_touched(), so take() is safe.
2026-02-09 09:35:03 +01:00
Alejandro Alonso e5cdb5b163 Merge pull request #8290 from penpot/alotor-fix-alt-duplicate
🐛 Fix problem with alt+move for duplicate shapes
2026-02-09 06:33:13 +01:00
David Barragán Merino a4f2641cc9 🔧 Enable observability for plugin docs and packages 2026-02-06 18:01:11 +01:00
Alejandro Alonso a164a1bab3 🐛 Fix dashboard navigation from workspace 2026-02-06 12:58:56 +01:00
alonso.torres a0cbb392af 🐛 Fix problem with alt+move for duplicate shapes 2026-02-06 12:20:43 +01:00
Alejandro Alonso ccfee34e76 Merge pull request #8289 from penpot/niwinz-staging-exporter-fix
🐛 Fix issue with pdf render on exporter
2026-02-06 11:40:18 +01:00
Andrey Antukh 989eb12139 🔥 Remove merge conflict from plugins api ns 2026-02-06 11:38:36 +01:00
Eva Marco a5e36dbb3d 🐛 Fix broken attribute on numeric input (#8250)
* 🐛 Fix broken attribute on numeric input

* 🐛 Fix tooltip position
2026-02-06 11:32:16 +01:00
Alejandro Alonso 8acd031ab2 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-06 11:23:50 +01:00
Andrey Antukh 6f3f2f9a71 🐛 Fix issue with pdf render on exporter
When paired with release build penpot app
2026-02-06 11:19:56 +01:00
Elena Torro a7c1de6478 🐛 Fix lazy load intersection on dragging at the beginning 2026-02-06 10:59:05 +01:00
Elena Torro 184487f568 🐛 Fix lazy load intersection on dragging at the beginning 2026-02-06 10:53:11 +01:00
Andrey Antukh c00d512193 Add the concept of version to plugins
And make mcp plugin version 2
2026-02-06 09:42:59 +01:00
alonso.torres af5dbf2fbc 🐛 Set objects modified instead of modif-tree 2026-02-06 09:34:58 +01:00
Alejandro Alonso 7c7e32d85f 🐛 Fix grid lines 2026-02-06 09:34:58 +01:00
Andrey Antukh 2ccb33ba89 📎 Add missing for-update for the migration 145 2026-02-05 18:12:11 +01:00
Andrey Antukh ee88ee63a2 Add data migration for fix plugins data on profiles 2026-02-05 18:08:28 +01:00
alonso.torres fd3d549f9c Batch text layout updates 2026-02-05 17:29:43 +01:00
alonso.torres 53c2acb3e6 🐛 Fix several problems with layouts and texts 2026-02-05 17:29:43 +01:00
Belén Albeza 8a72eb64c3 Add integration test for 13267 2026-02-05 16:37:21 +01:00
alonso.torres 1d45ca7019 🐛 Fix problem propagating geometry changes to instances 2026-02-05 16:37:21 +01:00
Eva Marco f961f9a123 🐛 Fix several bugs (#8267)
* ♻️ Remove rename warning

* 🐛 Fix opacity value
2026-02-05 11:34:14 +01:00
Eva Marco dda3377596 🐛 Allow detach broken token from input (#8242)
* 🐛 Allow detach broken token from input

* 🐛 Fix multiselection on multiple token applied

* ♻️ Remove detach-token new fn
2026-02-05 11:28:47 +01:00
Andrey Antukh 17935443df Move all tokenscript related adaptations to a separared package 2026-02-05 09:45:55 +01:00
Florian Schroedl 150d57b1eb Add tokenscript MVP 2026-02-05 09:45:55 +01:00
Alejandro Alonso ad5e8ccdb3 🐛 Fix pdf sizing issue on export (#8274) 2026-02-05 09:23:14 +01:00
Andrey Antukh 490619119e 🐛 Use correct listen address for mcp server 2026-02-04 18:57:53 +01:00
Belén Albeza 834b513562 🔧 Fix typo in workspace spec (#8272) 2026-02-04 17:05:49 +01:00
Andrey Antukh 1656fefdc9 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-04 16:23:46 +01:00
Andrey Antukh 7f318bb110 Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-04 16:22:13 +01:00
Andrey Antukh 44c7d3fbd6 Backport .github workflows from develop 2026-02-04 16:21:19 +01:00
Andrey Antukh 3d50aa6cb2 ⬆️ Update imagemagick version 2026-02-04 16:21:19 +01:00
Andrey Antukh 06afd94a74 ⬆️ Update backend dependencies (mainly bugfixes) 2026-02-04 16:21:19 +01:00
Andrey Antukh e7d9dca55e ⬆️ Update jdk and node on devenv and other images 2026-02-04 16:21:19 +01:00
Andrey Antukh c14ccc18b8 Import mcp from develop 2026-02-04 16:21:19 +01:00
Andrey Antukh ca4d00df69 🐛 Fix latest error report related migration 2026-02-04 15:36:07 +01:00
Andrey Antukh 9667477d6b 🐛 Add missing dep for rpc routes on backend 2026-02-04 15:26:02 +01:00
Alejandro Alonso 485005477e 🐛 Fix WasmWorkspacePage import 2026-02-04 14:02:38 +01:00
Alejandro Alonso 86ca260ea2 Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-04 13:50:13 +01:00
Andrey Antukh d80ba1856a Add several improvements to frontend error reporting
*  Add major improvement on error handling

*  Add the ability to store frontend reports

* 📎 Add PR feedback changes
2026-02-04 12:45:38 +01:00
Alejandro Alonso ebb7d01bc9 🐛 Fix entering decimal values in dimension fields causes internal server error (#8263) 2026-02-04 12:44:19 +01:00
Andrey Antukh a1bfb2781e 📎 Update mcp readme 2026-02-04 12:22:36 +01:00
Andrey Antukh 08e8787568 Add mcp types generator build script 2026-02-04 12:22:36 +01:00
Andrey Antukh da55653844 Add integration tests for mcp 2026-02-04 12:22:36 +01:00
Andrey Antukh 11f2323057 Add mcp to default compose template 2026-02-04 12:22:36 +01:00
Andrey Antukh ae0f5e2bb9 🐛 Fix subpath support on plugins 2026-02-04 12:22:36 +01:00
Andrey Antukh 1fff1f9506 Add mcp dockerfile 2026-02-04 12:22:36 +01:00
Andrey Antukh 61d7dd3167 Update devenv with mcp required dependencies 2026-02-04 12:22:36 +01:00
Dominik Jain 880b9b61c4 🎉 Integrate mcp repository
Original repository: https://github.com/penpot/penpot-mcp
Imported commit: fcfa67e908fc54e23a3a3543dee432472dc90c5d
2026-02-04 12:22:36 +01:00
Yamila Moreno 307dae9f61 💄 Remove access logs for /readyz 2026-02-04 10:54:07 +01:00
Xaviju 0f0ad4f161 🐛 Remove path from state when removing tokens (#8252)
* 🐛 Remove path from state when removing tokens

* ♻️ Improve path edition legibility

* ♻️ Fix path delete on change set
2026-02-04 10:15:46 +01:00
Alejandro Alonso 24c8fc484f 🐛 Fix Internal Error when adding a new text layer and trying to go back to Dashboard without saving 2026-02-04 10:01:10 +01:00
Eva Marco d6831e9b48 ♻️ Restore warning on name change in generic form (#8260) 2026-02-03 14:08:35 +01:00
Pablo Alba 138df7c958 🐛 Fix remove fill affects different element than selected (#8233) 2026-02-03 13:17:54 +01:00
alonso.torres ef2bdf86d8 Add event to create shape in plugins 2026-02-03 13:09:58 +01:00
alonso.torres 512a31d375 Add naturalChildOrdering flag to Plugin's API 2026-02-03 13:09:58 +01:00
Alejandro Alonso bc16b8ddc3 Merge pull request #8198 from penpot/ladybenko-13176-playwright-wasm
🔧 Migrate workspace tests to user the wasm viewport
2026-02-03 13:00:10 +01:00
Alejandro Alonso b07c98faa5 Merge pull request #8259 from penpot/superalex-improve-shadow-rendering
🎉 Improving shadow rendering performance
2026-02-03 12:59:49 +01:00
Alejandro Alonso 25aff100cf 🎉 Add shadows playground for render wasm 2026-02-03 12:44:43 +01:00
Alejandro Alonso 5be887f10b 🎉 Improve plain shape calculation 2026-02-03 12:44:43 +01:00
Alejandro Alonso f7403935c8 🎉 Improve shadows rendering performance 2026-02-03 12:33:05 +01:00
Andrey Antukh 7d09d930fe 📚 Update changelog 2026-02-03 11:13:46 +01:00
Belén Albeza 79be3ab7df 🔧 Fix text editor flaky tests 2026-02-03 10:39:38 +01:00
Andrey Antukh 717a048b73 📎 Add fmt fix on frontend 2026-02-03 09:37:19 +01:00
Andrey Antukh cbd90ff970 📎 Comment problematic code on frontend 2026-02-03 09:31:26 +01:00
Andrey Antukh c99fac000a Merge remote-tracking branch 'origin/staging-render' into develop 2026-02-03 09:30:16 +01:00
andrés gonzález 79e5d2f4cd 📚 Change link to post at SH guide (#8247) 2026-02-03 08:27:17 +01:00
Andrey Antukh 1325584e1a Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-03 08:24:04 +01:00
Andrey Antukh 0d9b7ca696 Merge tag '2.13.0-RC11' 2026-02-03 08:23:27 +01:00
Andrey Antukh d215a5c402 Merge tag '2.13.0-RC10' 2026-02-03 08:22:50 +01:00
Belén Albeza 629649aca6 🔧 Fix config playwright syntax 2026-02-02 16:25:16 +01:00
Belén Albeza cc326f23cf 🔧 Adjust timeout of websocket readiness (playwright) 2026-02-02 16:16:59 +01:00
Belén Albeza 2c4efc6b53 🔧 Fix onboarding test 2026-02-02 16:16:58 +01:00
Belén Albeza 4d5c874b91 🔧 Fix typography token test 2026-02-02 16:16:58 +01:00
Belén Albeza e3b97638b4 🔧 Fix broken / flaky tests 2026-02-02 16:16:58 +01:00
Belén Albeza daedc660b9 🔧 Migrate workspace tests to user the wasm viewport 2026-02-02 16:16:58 +01:00
Elena Torró 7681231d8f Merge pull request #8246 from penpot/azazeln28-test-more-text-editor
🔧 Add more Text Editor v2 tests
2026-02-02 15:09:18 +01:00
Aitor Moreno 07b9ef0fd6 🔧 Add more v2 text editor tests 2026-02-02 09:35:28 +01:00
Alejandro Alonso 2ae68d5752 Merge pull request #8244 from penpot/alotor-fix-modifiers-propagation
🐛 Fix problem with modifiers propagation
2026-01-29 17:34:36 +01:00
alonso.torres 913672e5c5 🐛 Fix problem with modifiers propagation 2026-01-29 17:15:01 +01:00
Xaviju 91671afb7a 🐛 Update switch checked state on change prop (#8235) 2026-01-29 13:13:50 +01:00
Eva Marco 838194f9e5 ♻️ Refactor code to reduce duplicates (#8213) 2026-01-29 12:28:05 +01:00
Alejandro Alonso 8c25fb00ac 🐛 Fix auto width/height texts on variant swithching 2026-01-29 12:25:38 +01:00
Alejandro Alonso 6a84215911 🐛 Fix stroke weight visually different with different levels of zoom 2026-01-29 12:18:26 +01:00
Xaviju 68cf2ecc57 🐛 Break long token names in remapping modal (#8241) 2026-01-29 11:04:36 +01:00
Xaviju 3e4f70f37b 🐛 Bulk remove tokens with a single undo action (#8208) 2026-01-29 10:58:16 +01:00
Andrey Antukh f65292a13c 📎 Mark as skip two text editor v2 tests (flaky) 2026-01-29 10:43:29 +01:00
Andrey Antukh 94722fdec2 Ensure .hidePopover fn exist before call 2026-01-29 10:43:29 +01:00
Andrey Antukh 28509e0418 Ensure .stopPropagation fn exists before calling
Also for .stopImmediatePropagation and .preventDefault on
the event instances.
2026-01-29 10:43:29 +01:00
Eva Marco 9569fa2bcb 🐛 Fix error when creating a token with an invalid name (#8216) 2026-01-29 10:41:52 +01:00
Eva Marco 852b31c3a0 🐛 Fix allow spaces on token description (#8234) 2026-01-29 10:40:32 +01:00
Eva Marco b8fdbd1ef8 🐛 Fix opacity value when token is broken (#8239) 2026-01-29 10:27:25 +01:00
Andrés Moya 84b3f5d7c6 🐛 Fix import of shadow tokens 2026-01-29 10:25:22 +01:00
Andrey Antukh 32454f5959 Merge remote-tracking branch 'origin/staging-render' into develop 2026-01-29 10:23:46 +01:00
Andrey Antukh b881e36875 Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-29 10:23:31 +01:00
Pablo Alba 0bb74ed722 🐛 Fix viewer can update library (#8231) 2026-01-28 20:48:52 +01:00
Andrey Antukh b40e775a70 Add minor improvements to performance events (#8217)
*  Move devtools perf logging helpers to util.perf ns

* 💄 Move flag check to the entry point instead of initialize event

* ♻️ Make performance events consistent with other events
2026-01-28 20:47:14 +01:00
Eva Marco 2b4e315744 ♻️ Replace layout item numeric inputs. (#8163)
*  Replace opacity numeric input

*  Add test

* ♻️ Replace margin inputs

* 🎉 Add test
2026-01-28 14:30:18 +01:00
Pablo Alba 4ca82821c1 🐛 Fix shared keys init should be by keywords (2) (#8230) 2026-01-28 13:41:37 +01:00
David Barragán Merino 76bd31fe7d 🔧 Fix CORS error 2026-01-28 13:40:45 +01:00
David Barragán Merino cc81e56d82 🔧 Fix CORS error 2026-01-28 13:40:26 +01:00
David Barragán Merino a90f672a5e 🔧 Fix CORS error 2026-01-28 13:30:08 +01:00
Aitor Moreno 2b00e4eec9 Merge pull request #8207 from penpot/alotor-wasm-disable-thumbnail-generation
🐛 Disable thumbnails render in wasm
2026-01-28 13:28:07 +01:00
Aitor Moreno 3b86d7c1b1 🐛 Fix initializing rasterizer 2026-01-28 12:59:16 +01:00
alonso.torres 3cb716ec30 🐛 Disable thumbnails render in wasm 2026-01-28 12:59:16 +01:00
Andrey Antukh a9e2fc8d94 Backport linter fixes and config from develop 2026-01-28 12:58:54 +01:00
Pablo Alba f76598f638 🐛 Fix shared keys init should be by keywords (#8228) 2026-01-28 12:56:04 +01:00
Andrey Antukh 17ffd9a5d0 Backport linter fixes and config from develop 2026-01-28 12:54:18 +01:00
Xaviju eacc033567 🐛 Fix long token names overflow remap modal (#8224) 2026-01-28 12:44:07 +01:00
Andrey Antukh 71c349479f Merge pull request #8196 from penpot/niwinz-develop-management-auth-changes
♻️ Make several improvements to management API authentication
2026-01-28 10:52:26 +01:00
David Barragán Merino 77bbf30ae4 🔧 Fix file name 2026-01-27 21:16:44 +01:00
David Barragán Merino 693b52bf45 📚 Fix links related to penpot plugins 2026-01-27 21:16:44 +01:00
David Barragán Merino 0f51b23ce7 🔧 Deploy plugin styles documentation 2026-01-27 21:16:44 +01:00
David Barragán Merino ec61aa6b6d 🔧 Add custom domain 2026-01-27 21:16:41 +01:00
David Barragán Merino 18aca16f98 🔧 Fix file name 2026-01-27 21:12:32 +01:00
David Barragán Merino c6465e27e3 📚 Fix links related to penpot plugins 2026-01-27 21:12:32 +01:00
David Barragán Merino 1834a18263 🔧 Deploy plugin styles documentation 2026-01-27 21:12:32 +01:00
David Barragán Merino d220d07875 🔧 Add custom domain 2026-01-27 21:12:32 +01:00
David Barragán Merino fda31624c1 🔧 Fix file name 2026-01-27 21:04:25 +01:00
David Barragán Merino 7f640569bd 📚 Fix links related to penpot plugins 2026-01-27 20:59:54 +01:00
David Barragán Merino 91f1323802 🔧 Deploy plugin styles documentation 2026-01-27 20:59:54 +01:00
David Barragán Merino dbd4a2366f 🔧 Add custom domain 2026-01-27 20:59:54 +01:00
Pablo Alba cbb6d098a7 🐛 Fix boolean operators in menu for boards (#8177) 2026-01-27 17:58:07 +01:00
Andrey Antukh b6f5000d1c ⬆️ Update pnpm 2026-01-27 17:57:07 +01:00
Andrey Antukh 0527124f2f Merge remote-tracking branch 'origin/staging-render' into develop 2026-01-27 17:56:03 +01:00
Andrey Antukh faf91ac70d Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-27 17:53:16 +01:00
Eva Marco 9ca76c745f 🐛 Fix app freeze on token name change (#8214) 2026-01-27 17:31:50 +01:00
Andrey Antukh 89935e2174 Make nitrate module loading conditional to flag
This removes the flag checking on each rpc method
2026-01-27 15:16:36 +01:00
Andrey Antukh 7f27e0326d Reuse basic team and profile schemas on nitrate 2026-01-27 15:14:32 +01:00
Andrey Antukh 9c539dfb2f 🔥 Remove subscriptions related management module 2026-01-27 15:14:32 +01:00
Andrey Antukh 50a4cf8b99 📎 Adapt nitrate module to auth changes 2026-01-27 15:14:32 +01:00
Andrey Antukh f5996a7235 ♻️ Make several improvements to management API authentication 2026-01-27 15:14:32 +01:00
Andrey Antukh e8fd4698c9 🔧 Update caddy configuration 2026-01-27 15:10:53 +01:00
Andrey Antukh 0ab126748f 💄 Add format rule for code comments (#8211)
* 💄 Add format rule for code comments

* ⬆️ Update linter and formatter on devenv
2026-01-27 15:07:18 +01:00
Yamila Moreno 71a5ab9913 🔧 Delete unused workflow 2026-01-27 13:47:05 +01:00
Andrey Antukh 61969f3eb5 Improve unhandled exception handling 2026-01-27 13:46:51 +01:00
Andrey Antukh bd2ef8057e Add helper for proper print js exceptions 2026-01-27 13:46:51 +01:00
Elena Torró 9808b6ca57 Merge pull request #8205 from penpot/superalex-improve-huge-shapes-render
🎉 Improving huge shapes render
2026-01-27 13:08:25 +01:00
Eva Marco 2523096fdd 🐛 Fix css rule (#8206) 2026-01-27 12:30:14 +01:00
Aitor Moreno de41cb5488 🐛 Fix add/remove fills to text nodes 2026-01-27 12:17:10 +01:00
Xaviju 8e63c4e3e8 ♻️ Review remap interface and interaction (#8168)
* ♻️ Review remap interface and interaction
* ♻️ Fix remapping feature tests
2026-01-27 11:18:34 +01:00
Alejandro Alonso b40ccaf030 🎉 Improve zoom actions for huge shapes 2026-01-27 11:11:38 +01:00
Alejandro Alonso 7d3ac38749 🎉 Improve huge shapes rendering 2026-01-27 11:11:38 +01:00
d5abc52dac 🎉 Add first integration with nitrate (#7803)
* 🐛 Display missing selected tokens set info (#8098)

* 🐛 Display missing selected tokens set info

*  Add integration tests to verify current active set

* 🎉 Integration with nitrate platform

* 🐛 Fix nitrate get-teams returns deleted teams

*  Add nitrate to tmux devenv

*  Add retry and validation to nitrate module

*  Add photoUrl to profile on nitrate authenticate

*  Move nitrate url to an env variable

* ♻️ Change Nitrate organization-id schema to text

* ♻️ Cleanup unused imports

* 🔧 Add control-center to nginx

*  Add create org link

* 🔧 Fix nginx entrypoint

* 🐛 Fix control-center proxy pass

* 🎉 Add nitrate licence check

* Revert " Add nitrate to tmux devenv"

This reverts commit dc6f6c458995dac55cab7be365ced0972760a058.

*  Add feature flag check

* 🐛 Rename licences for licenses

*  MR changes

*  MR changes 2

* 📎 Add the ability to have local config on start backend

* 📎 Add FIXME comment

---------

Co-authored-by: Xaviju <xavier.julian@kaleidos.net>
Co-authored-by: Juanfran <juanfran.ag@gmail.com>
Co-authored-by: Yamila Moreno <yamila.moreno@kaleidos.net>
Co-authored-by: Marina López <marina.lopez.yap@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-01-27 10:04:53 +01:00
Elena Torro 8d1bc6c50c 🐛 Fix flex layout sorting on reverse order with no z-index 2026-01-27 09:34:36 +01:00
Andrey Antukh 3112b240a0 📎 Add missing entry on changelog 2026-01-27 09:28:41 +01:00
Andrey Antukh 56fd66b91a 🐛 Fix several issues related to path edition (#8187)
*  Improve save-path-content event consistency

Mainly removing possible race conditions from the event
implementation.

*  Ensure path content snapshot on start-path-edit event

*  Reuse already available shape-id on split-segments
2026-01-27 09:27:42 +01:00
Andrey Antukh 3b96eb5476 🐛 Fix incorrect handling of schema expression on obj/reify 2026-01-27 09:20:33 +01:00
Elena Torro 2a7c24f6fd 🐛 Fix shape operations on sidebar when using interaction observer 2026-01-27 09:03:41 +01:00
Andrey Antukh 7a842ce36a Restore back node based http server for e2e tests 2026-01-27 08:55:17 +01:00
Andrey Antukh ea25c5db99 🔥 Remove unused svg from frontend resources 2026-01-27 08:55:17 +01:00
Alejandro Alonso 947aa22dee Merge pull request #8173 from penpot/elenatorro-improve-surface-performance
🔧 Improve surface rendering performance
2026-01-27 07:21:23 +01:00
Alejandro Alonso ce1796eb02 Merge pull request #8199 from penpot/elenatorro-13192-fix-layer-items-operations
🐛 Fix shape operations on sidebar when using interaction observer
2026-01-27 07:14:55 +01:00
Andrey Antukh 6f0685ba8e 🐛 Fix several issues related to path edition (#8187)
*  Improve save-path-content event consistency

Mainly removing possible race conditions from the event
implementation.

*  Ensure path content snapshot on start-path-edit event

*  Reuse already available shape-id on split-segments
2026-01-26 22:48:57 +01:00
Andrey Antukh abc1773f65 Merge tag '2.13.0-RC9' 2026-01-26 18:12:16 +01:00
David Barragán Merino 93f5e74bb0 🔧 Run all the jobs if the workflow is launched manually 2026-01-26 17:14:15 +01:00
David Barragán Merino 1ce0b60e3d 🔧 Run all the jobs if the workflow is launched manually 2026-01-26 17:13:54 +01:00
David Barragán Merino d433fd25c1 🔧 Run all the jobs if the workflow is launched manually 2026-01-26 17:12:58 +01:00
Elena Torro bb0e9b47cb 🐛 Fix shape operations on sidebar when using interaction observer 2026-01-26 16:14:18 +01:00
Elena Torro 5209a8b423 🔧 Improve surface rendering performance 2026-01-26 16:10:22 +01:00
Aitor Moreno f4f4f5bbb5 🐛 Fix multiple issues and tests 2026-01-26 14:14:06 +01:00
David Barragán Merino 38179ba11e 🔧 Enable secret inheritance 2026-01-26 14:01:22 +01:00
David Barragán Merino ef80901400 🔧 Enable secret inheritance 2026-01-26 14:00:55 +01:00
David Barragán Merino c5f03d711a 🔧 Enable secret inheritance 2026-01-26 14:00:09 +01:00
David Barragán Merino 719a95246a 🔧 Define deploy plugin packages workflows 2026-01-26 13:48:33 +01:00
David Barragán Merino e590cd852d 🔧 Rename wrangle to wrangler 2026-01-26 13:48:33 +01:00
David Barragán Merino a9741073e5 🔧 Add deploy plugin packages workflow placeholder and wrangle config files 2026-01-26 13:48:33 +01:00
David Barragán Merino 5306bed548 🔧 Define deploy plugin packages workflows 2026-01-26 13:47:57 +01:00
David Barragán Merino 92a319ddd1 🔧 Rename wrangle to wrangler 2026-01-26 13:47:57 +01:00
David Barragán Merino 68a6d4c9a8 🔧 Add deploy plugin packages workflow placeholder and wrangle config files 2026-01-26 13:47:57 +01:00
David Barragán Merino 72cc5ee349 🔧 Define deploy plugin packages workflows 2026-01-26 13:23:46 +01:00
Eva Marco 804695b48b ♻️ Replace stroke width numeric inputs (#8137)
*  Replace opacity numeric input

*  Add test

* ♻️ Replace stroke width numeric input

* 🎉 Add tests
2026-01-26 12:50:28 +01:00
Elena Torró 20c8fbf314 🔧 Make render-wasm visual regression tests use gpu (#8189) 2026-01-26 11:53:29 +01:00
Andrey Antukh e02536f8d4 Merge branch 'staging-render' into develop 2026-01-26 11:02:50 +01:00
Andrey Antukh 3eeaaab17e Merge branch 'staging' into staging-render 2026-01-26 11:02:26 +01:00
Andrey Antukh f07495ae95 🐛 Fix incorrect handling of numeric values layout padding and gap
Fixes https://github.com/penpot/penpot/issues/8113
2026-01-26 11:01:25 +01:00
Andrey Antukh 23d5fc7408 🐛 Prevent exception on open-new-window when no window is returned
Fixes https://github.com/penpot/penpot/issues/7787
2026-01-26 11:01:25 +01:00
Andrey Antukh 8632b18eec 🐛 Avoid json decoder liner limit exception by chunking
Happens only when we send large binary data serialized with transit
(mainly used for upload fonts data).
2026-01-26 11:01:25 +01:00
Andrey Antukh 33e650242c Add slugify to the filename on assets exportation
Fixes https://github.com/penpot/penpot/issues/8017
2026-01-26 11:01:25 +01:00
Alejandro Alonso 3dc9e28230 Merge pull request #8155 from penpot/elenatorro-13089-improve-page-load-render
🔧 Improve render UX on first load
2026-01-26 10:40:44 +01:00
Andrey Antukh e03ad25118 🔧 Backport CI workflow from develop 2026-01-26 10:18:24 +01:00
Andrey Antukh d9c56da705 ⬆️ Update playwright on frontend module 2026-01-26 10:08:53 +01:00
Andrey Antukh 75248aec4e 🔧 Add missing container on tests.yml 2026-01-26 09:52:47 +01:00
David Barragán Merino f0d9429775 🔧 Rename wrangle to wrangler 2026-01-26 09:37:33 +01:00
Eva Marco 62ecf48bdb 🐛 Fix glich when applying padding (#8099)
*  Replace opacity numeric input

*  Add test

* 🐛 Fix glich when applying padding
2026-01-26 08:40:32 +01:00
Alejandro Alonso 18de7f1db6 Merge pull request #8041 from penpot/niwinz-subpath-support
 Add several adjustments for make penpot run on subpath
2026-01-26 07:34:24 +01:00
Alejandro Alonso 2b2941bd25 Merge pull request #8049 from penpot/niwinz-virtual-clock-by-user
 Make the virtual clock by profile and not global
2026-01-26 07:22:08 +01:00
Alejandro Alonso f2d561eff7 Merge pull request #8063 from penpot/niwinz-develop-auditlog-logging
 Add the ability to log using logging subsystem the audit events
2026-01-26 07:17:13 +01:00
Alejandro Alonso 418b65a287 Merge pull request #8143 from penpot/niwinz-develop-bugfix-2
🐛 Fix incorrect handling of numeric values layout padding and gap
2026-01-26 07:11:09 +01:00
Alejandro Alonso d4e7810eba Merge pull request #8135 from penpot/niwinz-develop-chunking-on-font-upload
🐛 Avoid json decoder line limit exception by chunking
2026-01-26 07:08:23 +01:00
Andrey Antukh 1d1d32ad39 🐛 Avoid json decoder liner limit exception by chunking
Happens only when we send large binary data serialized with transit
(mainly used for upload fonts data).
2026-01-26 06:58:17 +01:00
Alejandro Alonso fb08dc65c8 Merge pull request #8133 from penpot/niwinz-develop-asset-export-whitespaces
 Add slugify to the filename on assets exportation
2026-01-26 06:55:52 +01:00
Alejandro Alonso 927ac93fa7 Merge pull request #8141 from penpot/niwinz-develop-bugfix-1
🐛 Prevent exception on open-new-window when no window is returned
2026-01-26 06:32:02 +01:00
Andrey Antukh e546a7c614 🐛 Prevent exception on open-new-window when no window is returned
Fixes https://github.com/penpot/penpot/issues/7787
2026-01-26 06:31:40 +01:00
David Barragán Merino 058c20c2e2 🔧 Add deploy plugin packages workflow placeholder and wrangle config files 2026-01-23 20:39:39 +01:00
David Barragán Merino 599656c31e 🔧 Fix a typo in an interpolation 2026-01-23 19:52:47 +01:00
David Barragán Merino 5d7e6afd76 🔧 Fix a typo in an interpolation 2026-01-23 19:52:20 +01:00
Elena Torró 68a77e9cc8 Merge pull request #8179 from penpot/superalex-adding-performance-logs-flag
🎉 Adding performance logs flag
2026-01-23 14:06:57 +01:00
Alejandro Alonso e3148ea20e 🎉 Adding performance logs flag 2026-01-23 13:34:19 +01:00
Elena Torró 5da9bbea62 Merge pull request #8174 from penpot/superalex-fix-blur-events-text-editor-v2
🐛 Fix blur events for text editor v2 in firefox
2026-01-23 13:08:01 +01:00
Alonso Torres 15d369493b 🐛 Fix problem with z-index modal in dashboard (#8178) 2026-01-23 12:48:01 +01:00
Andrey Antukh 5016b2a7bf Merge remote-tracking branch 'origin/staging-render' into develop 2026-01-23 11:18:33 +01:00
Andrey Antukh 089d1667b6 Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-23 11:08:07 +01:00
Alejandro Alonso 4ad5282063 🐛 Fix blur events for text editor v2 in firefox 2026-01-23 10:58:54 +01:00
Elena Torró d0e79c94b4 Merge pull request #8162 from penpot/superalex-fix-auto-height
🐛 Fix text boxes with auto-height don't update height when resized by dragging side handles
2026-01-23 10:57:54 +01:00
Eva Marco 9c9b672e3e 🐛 Fix spanish translations on import export token modal (#8172) 2026-01-23 10:05:20 +01:00
Andrey Antukh 43ae213659 Replace login illustration svg with a correct bitmap (#8170) 2026-01-23 09:59:29 +01:00
Eva Marco 5146221513 🐛 Fix allow negative spread values on shadow token creation (#8167)
* 🐛 Fix allow negative spread values on shadow token creation

* 🎉 Add test
2026-01-23 09:50:36 +01:00
Eva Marco e53f335204 🐛 Fix unhandled error on tokens modal (#8165) 2026-01-23 09:35:53 +01:00
Alejandro Alonso d112c0a33b 🐛 Fix text boxes with auto-height don't update height when resized by dragging side handles 2026-01-23 09:05:20 +01:00
Elena Torró 7b86518afa Merge pull request #8171 from penpot/ladybenko-13152-fix-blur
🐛 Fix blur when clicking on same page
2026-01-22 17:42:39 +01:00
Elena Torró 9991901ed8 Merge pull request #8161 from penpot/superalex-fix-editing-text-doesnt-update-layer-name
🐛 Bug: Editing the text inside a text object doesn’t update the text layer name.
2026-01-22 17:40:32 +01:00
Belén Albeza 3d0c6ad421 Blur board titles and outlines when switching pages 2026-01-22 16:00:24 +01:00
Andrey Antukh dc973dac36 🔧 Enable link workspace packages on plugins 2026-01-22 13:55:41 +01:00
Andrey Antukh 4467827218 ⬆️ Update react dependency on frontend 2026-01-22 13:55:41 +01:00
Andrey Antukh 6470db8d5f Remove mention of yarn on several files 2026-01-22 13:55:41 +01:00
Andrey Antukh dc44156b53 🐛 Fix text editor wasm playground 2026-01-22 13:55:41 +01:00
Andrey Antukh f0e53d70ae 🎉 Migrate to PNPM render-wasm module 2026-01-22 13:55:41 +01:00
Andrey Antukh ef73a263b2 🎉 Migrate to PNPM docs module 2026-01-22 13:55:41 +01:00
Andrey Antukh 9b1e007a49 Replace e2e node server with caddy
Which is already available in the devenv runtime image
2026-01-22 13:55:41 +01:00
Andrey Antukh ea8632e56a 🐛 Fix frontend test-components scripts 2026-01-22 13:55:41 +01:00
Andrey Antukh 2d00e64ede Fix e2e scripts related to pnpm change 2026-01-22 13:55:41 +01:00
Andrey Antukh 1246250198 🎉 Migrate to PNPM frontend module 2026-01-22 13:55:41 +01:00
Andrey Antukh 34f2943dcd 🎉 Migrate to PNPM library module 2026-01-22 13:55:41 +01:00
Andrey Antukh 3fb78116b8 🎉 Migrate to PNPM backend module 2026-01-22 13:55:41 +01:00
Andrey Antukh 072e415b9e 🎉 Migrate to PNPM common module 2026-01-22 13:55:41 +01:00
Andrey Antukh 67a904824c 🎉 Migrate to PNPM exporter module 2026-01-22 13:55:41 +01:00
Belén Albeza 835ea97be7 🐛 Fix blur applied when clicking in the active page 2026-01-22 13:27:05 +01:00
Andrey Antukh 68184209be Add the ability to make json/->clj non-recursive 2026-01-22 13:07:05 +01:00
Andrey Antukh d2295862b4 Add the ability to customize decoder and schema on obj/reify 2026-01-22 13:07:05 +01:00
Andrey Antukh 23cbf33d1b Merge remote-tracking branch 'origin/staging' into develop 2026-01-22 12:34:49 +01:00
David Barragán Merino 16f22a7b5c 🔧 Fixes to the API documentation deployer 2026-01-22 12:10:27 +01:00
David Barragán Merino a1460115e8 🔧 Deploy penpot api documentation 2026-01-22 12:10:27 +01:00
David Barragán Merino 2574ad3315 🔧 Fixes to the API documentation deployer 2026-01-22 12:09:38 +01:00
David Barragán Merino e6b5364a84 🔧 Deploy penpot api documentation 2026-01-22 12:09:38 +01:00
David Barragán Merino b4ff0ccf3a 🔧 Fixes to the API documentation deployer 2026-01-22 12:08:09 +01:00
Andrey Antukh 6c6666a39a Merge pull request #8158 from penpot/ladybenko-13058-hide-avatar
🐛 Do not hide active users avatar when there are 3 of them
2026-01-22 12:02:53 +01:00
Elena Torro f94c9cdb02 🐛 Fix objects sorting for thumbnail generation 2026-01-22 09:29:33 +01:00
Elena Torro 8637c46ba1 🐛 Fix empty pool state 2026-01-22 08:52:26 +01:00
Elena Torro 5d7d23a2c7 🔧 Keep clear cached canvas 2026-01-22 08:51:58 +01:00
Alejandro Alonso a1a3966d7b 🐛 Editing the text inside a text object doesn’t update the text layer name 2026-01-22 08:24:13 +01:00
David Barragán Merino c1335961b4 🔧 Deploy penpot api documentation 2026-01-21 18:52:07 +01:00
Alonso Torres 656f81f89f ⬆️ Update plugins to 1.4.2 (#8157) 2026-01-21 17:36:58 +01:00
Belén Albeza eaf64b6e16 ♻️ Make the CSS of presence widgets to adhere to our guidelines 2026-01-21 17:33:18 +01:00
Belén Albeza 560a0d09d5 🐛 Fix hiding avatar when we have 3 active users 2026-01-21 16:01:20 +01:00
Elena Torro aab1d97c4c 🔧 Clean up and use proper imports 2026-01-21 16:01:06 +01:00
Elena Torro 499aac31a4 🔧 Improve tile invalidation to prevent visual flickering
When tiles are invalidated (during shape updates or page loading), the old tile
content is now kept visible until new content is rendered to replace it. This
provides a smoother visual experience during updates.
2026-01-21 15:42:52 +01:00
Alonso Torres 01a4ffeb8b ⬆️ Updated plugins release to 1.4.0 (#8148) 2026-01-21 15:41:00 +01:00
Elena Torro 962d7839a2 🔧 Add progressive rendering support for improved page load experience
When loading large pages with many shapes, the UI now remains responsive by
processing shapes in chunks (100 shapes at a time) and yielding to the browser
between chunks. Preview renders are triggered at 25%, 50%, and 75% progress to
give users visual feedback during loading.
2026-01-21 14:55:53 +01:00
Elena Torro 83387701a0 🔧 Add batched shape base properties serialization for improved WASM performance 2026-01-21 14:55:07 +01:00
Elena Torro 5775fa61ba 🔧 Refactor ShapesPool to use index-based storage instead of unsafe lifetime references
Replace `HashMap<&'a Uuid, ...>` with `HashMap<usize, ...>` for all auxiliary maps
(modifiers, structure, scale_content, modified_shape_cache)
2026-01-21 14:53:56 +01:00
Andrey Antukh b70eb768e0 Merge remote-tracking branch 'origin/staging' into develop 2026-01-21 13:51:41 +01:00
Andrey Antukh b8c70be9a2 Make frontend build and watch process more resilent to errors 2026-01-21 13:44:35 +01:00
Andrey Antukh 525adcfcbe Add wasm build on watch app script (devenv) 2026-01-21 13:44:35 +01:00
Eva Marco 7cce4c6532 🐛 Fix unhandled exception tokens creation dialog (#8136) 2026-01-21 13:09:22 +01:00
Alejandro Alonso a3fdd8b691 Merge pull request #8147 from penpot/niwinz-staging-file-menu-issue
 Use correct team-id on file-menu on dashboard
2026-01-21 12:54:28 +01:00
Andrey Antukh b6a9579c98 Use correct team-id on file-menu on dashboard
Before the changes on this commit, the team object is used for
retrieve the id, where we already have team-id. Additionally, the
team object resolution is async operation and is not available on
the first render which causes strange issues on automated flows
(playwright) where an option is clicked when the async flow is
still pending and we have no team object loaded.
2026-01-21 12:23:44 +01:00
Belén Albeza 5b1766835f 🐛 Fix broken selection on duplicated shapes on new pages 2026-01-21 10:32:13 +01:00
Andrey Antukh 4397ede5c1 Merge branch 'staging-render' into develop 2026-01-21 10:18:15 +01:00
Andrey Antukh ff25df0457 Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-21 10:17:22 +01:00
Andrey Antukh e0910db99e 🐛 Fix incorrect handling of numeric values layout padding and gap
Fixes https://github.com/penpot/penpot/issues/8113
2026-01-21 09:38:44 +01:00
Eva Marco 8c7fd0af4b 🐛 Fix shadow reference validation (#8132) 2026-01-21 09:17:03 +01:00
Andrey Antukh cf46051f56 🔥 Remove .traivis.yml file from the repository 2026-01-20 19:40:31 +01:00
Luis de Dios 079b3fbfad ♻️ Extract and create panel title component (#8090) 2026-01-20 18:56:25 +01:00
Andrey Antukh 299f628951 Merge pull request #8123 from penpot/GlobalStar117-fix/token-validation-crash
🐛 Fix Penpot crash when setting some name in Design tokens
2026-01-20 18:53:05 +01:00
David Barragán Merino 32d0fe6463 🔧 Use selfhosted runner 01 to generate the bundle 2026-01-20 18:09:18 +01:00
Andrey Antukh 6393330ee1 Merge remote-tracking branch 'origin/staging' 2026-01-20 16:25:10 +01:00
Andrey Antukh 8252bc485e 📚 Fix oidc callback related documentation issue 2026-01-20 16:24:12 +01:00
Andrey Antukh cecd3d4a90 📎 Update changelog 2026-01-20 16:00:57 +01:00
Eva Marco 1c2c0987f5 🐛 Fix schema validation for references from other sets 2026-01-20 15:51:43 +01:00
Globalstar117 0418147e74 🐛 Add error handler to token form validation to prevent crash
When creating a token with a name that conflicts with existing
hierarchical token names (e.g., 'accent-color' when 'accent-color.blue.dark'
exists), the validation throws an error via rx/throw. However, the
rx/subs! subscriber in generic_form.cljs had no error handler, causing
an unhandled exception that resulted in an 'Internal Error' crash.

This fix adds an error handler that:
1. Catches validation errors from the reactive stream
2. Uses humanize-errors to convert them to user-friendly messages
3. Displays the error in the form's extra-errors field

Before: Crash with 'Internal Error' dialog
After: Form shows validation error message

Fixes #8110

---
This is a Gittensor contribution.
gittensor:user:GlobalStar117
2026-01-20 15:51:25 +01:00
Andrey Antukh 9e0ba4429a Add slugify to the filename on assets exportation
Fixes https://github.com/penpot/penpot/issues/8017
2026-01-20 15:27:24 +01:00
Alonso Torres 47775a9e2c Merge pull request #8134 from penpot/alotor-fix-plugins-export
🐛 Fix problem with export in plugins
2026-01-20 15:03:04 +01:00
Eva Marco 7499a5bca6 ♻️ Replace opacity input (#8072)
*  Replace opacity numeric input

*  Add test
2026-01-20 14:30:35 +01:00
Xaviju 6cd5bc76d7 💄 Replace current themes switch with DS switch (#8131) 2026-01-20 14:17:25 +01:00
Andrey Antukh 8191d04114 Use non-legacy config example on docker compose file 2026-01-20 13:25:55 +01:00
David Barragán Merino bbe6ee2e19 🔧 Define a different temporary config file for each execution 2026-01-20 12:59:56 +01:00
David Barragán Merino fb6d8309b6 🔧 Prevent error 429 downloading docker images from dockerhub 2026-01-20 12:59:56 +01:00
Alejandro Alonso b7c2d9a079 Merge pull request #8130 from penpot/superalex-improve-zoom-pan-performance-7
🐛 Fix some tiles disappear after fast zoom and pan
2026-01-20 12:56:02 +01:00
Alejandro Alonso aeb34a6f64 Merge pull request #8109 from penpot/superalex-fix-text-selrect-calculation
🐛 Render wasm typography token issues
2026-01-20 12:54:45 +01:00
Alejandro Alonso 6fa0c3af0c 🐛 Fix some tiles disappear after fast zoom and pan 2026-01-20 12:40:01 +01:00
Alejandro Alonso 260b9fb040 🐛 Fix texts with auto size updated via tokens with render wasm
activated
2026-01-20 12:39:17 +01:00
Alejandro Alonso 884954f4ff 🐛 Fix text selrect calculation 2026-01-20 12:37:57 +01:00
Andrey Antukh 88f0f75174 Merge pull request #8129 from penpot/niwinz-staging-bugfix-1
 Several improvements for build process
2026-01-20 12:26:55 +01:00
Andrey Antukh 1ffa956251 Include timestamp on version tag 2026-01-20 12:26:39 +01:00
Andrey Antukh 31054099ff Use pseudo-names on release builds of frontend (#8105) 2026-01-20 12:26:39 +01:00
Andrey Antukh 689467bcf9 📎 Update changelog 2026-01-20 12:25:43 +01:00
Andrey Antukh 7724450037 Merge remote-tracking branch 'origin/staging-render' into develop 2026-01-20 12:18:04 +01:00
Xaviju 368fa954ce Remove tokens tree node (#8042) 2026-01-20 11:00:13 +01:00
Eva Marco 983487d73c 🐛 Fix shadow token reference validation (#8128) 2026-01-20 10:56:27 +01:00
Andrey Antukh 6fd0f5377c Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-20 10:08:58 +01:00
Elena Torró eb54bc485e Merge pull request #8120 from penpot/alotor-fix-flex-layout
🐛 Fix problems with layout
2026-01-20 10:00:24 +01:00
Elena Torró 12c24a36b4 Merge pull request #8122 from penpot/fix-thumbnail-generation
🐛 Fix problem with thumbnail generation
2026-01-20 09:59:34 +01:00
Alejandro Alonso 324d54ad28 🐛 Fix set all rounded corners to 0 2026-01-20 09:34:06 +01:00
alonso.torres f42ff27f3d 🐛 Fix problem with bools 2026-01-19 17:05:04 +01:00
Andrey Antukh 0ecb2bc838 Merge remote-tracking branch 'origin/staging' into develop 2026-01-19 13:42:05 +01:00
Andrey Antukh 3ecf509f3b 🔧 Add missing gif files handling on defaut nginx config 2026-01-19 13:32:01 +01:00
alonso.torres 2c1cc89f53 🐛 Fix problem with thumbnail generation 2026-01-19 12:54:15 +01:00
alonso.torres 498b0b30fe 🐛 Fix problems with layout 2026-01-19 12:17:58 +01:00
Elena Torró 89f40dcda2 🔧 Move WebGL context error message to 'errors' namespace (#8117) 2026-01-19 11:24:19 +01:00
Andrey Antukh e92f3fb3cb Use pseudo-names on release builds of frontend (#8105) 2026-01-19 11:23:35 +01:00
Andrey Antukh 5193cfd56e :paerclip: Update changelog 2026-01-19 11:15:39 +01:00
Dalai FelintoandDalai Felinto 7f395b2642 🐛 Fix import tokens dialog default option (#8051)
This was intended to be changed on 13fcf3a9bb. However only the menu
order changed, not the default option.

Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
2026-01-19 11:14:34 +01:00
Dalai FelintoandDalai Felinto 813d5d8e69 🐛 Fix import tokens dialog default option (#8051)
This was intended to be changed on 13fcf3a9bb. However only the menu
order changed, not the default option.

Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
2026-01-19 11:13:55 +01:00
Andrey Antukh 84f1ff092d Merge remote-tracking branch 'origin/staging' into develop 2026-01-19 11:10:37 +01:00
Juan de la Cruz 2a62bd2586 🎉 Add 2.13 release slides (#8116) 2026-01-19 11:09:58 +01:00
Elena Torró ccac7bd510 Merge pull request #8108 from penpot/ladybenko-13022-blur-page
🎉 Apply blur effect when switching pages
2026-01-19 11:04:31 +01:00
Andrey Antukh f2b082b93e Merge branch 'staging-render' into develop 2026-01-19 10:54:37 +01:00
Andrey Antukh d73197625d Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-19 10:43:43 +01:00
Andrey Antukh 1f41bef4a9 Add several adjustments for make penpot run on subpath 2026-01-18 10:12:23 +01:00
Marina López fdf5bb250b 🐛 Fix profile not updating after deleting a team member (#8075) 2026-01-18 10:08:18 +01:00
Yamila Moreno 786736fadd Improve default nginx config (#8104) 2026-01-18 10:07:44 +01:00
Pablo Alba 1ff6e00398 🐛 Fix error message on components doesn't close automatically (#8081) 2026-01-18 10:07:11 +01:00
David Barragán Merino 25455523ad 🔧 Revert use of selfhosted runner 02 to generate the bundle 2026-01-16 17:51:02 +01:00
David Barragán Merino 8dfeb21978 🔧 Use selfhosted runner 02 to generate the bundle and the docker images 2026-01-16 17:44:15 +01:00
Belén Albeza 43d1d127dc 🎉 Apply blur effect to previous canvas pixels while setting wasm objects 2026-01-16 13:04:59 +01:00
Belén Albeza 8bd3ef717c 🎉 Apply blur to canvas when switching pages 2026-01-16 13:04:59 +01:00
Elena Torro 53bc647783 🔧 Fix shape selection from canvas to sidebar 2026-01-16 13:02:25 +01:00
Alejandro Alonso ad2833bb7a Merge pull request #8107 from penpot/elenatorro-13077-improve-shape-selection-performance
🔧 Fix shape selection from canvas to sidebar
2026-01-16 13:01:58 +01:00
Elena Torro 21911e898f 🔧 Fix shape selection from canvas to sidebar 2026-01-16 12:45:45 +01:00
Andrey Antukh 538073debf Merge remote-tracking branch 'origin/staging' into develop 2026-01-16 10:38:20 +01:00
Andrey Antukh 1ae7515189 🐛 Set correct defaults for storage configuration on compose (#8060)
* 🔥 Remove mention to legacy and never released admin on compose

* 🐛 Set correct defaults for storage configuration on compose
2026-01-16 08:11:34 +01:00
andrés gonzález 8c401f5346 📚 Add Files and Projects section to user guide (#8095) 2026-01-15 17:01:05 +01:00
Elena Torró 6029f9bb51 Merge pull request #8089 from penpot/superalex-improve-zoom-pan-performance-5
🎉 Performance improvements
2026-01-15 16:46:07 +01:00
Elena Torro e0fd8bac81 🔧 Optimize sidebar performance for deeply nested shapes
- Batch hover highlights using RAF to avoid long tasks from rapid events
- Run parent expansion asynchronously to not block selection
- Lazy-load children in layer items using IntersectionObserver
- Clarify expand-all-parents logic with explicit bindings
2026-01-15 13:41:54 +01:00
Elena Torro 34737ddfc9 🔧 Always lookup over a set 2026-01-15 13:41:10 +01:00
Elena Torro a8dfd19338 🔧 Add performance debugging logs 2026-01-15 13:40:58 +01:00
Elena Torro e33e8a8c3b 🔧 Lookup page objects only when value changes 2026-01-15 13:40:53 +01:00
Alejandro Alonso 214b0efa02 Merge pull request #8074 from penpot/elenatorro-13017-improve-sidebar-performance
🔧 Optimize sidebar performance for deeply nested shapes
2026-01-15 13:28:14 +01:00
David Barragán Merino 661436ecae 🔧 Use selfhosted runner 02 2026-01-15 13:26:49 +01:00
Alejandro Alonso c411aefc6c 🐛 Fix rotated shapes extrect calculation 2026-01-15 12:53:21 +01:00
Alejandro Alonso 311e124658 🎉 Reduce extrect work in tile traversal
Avoid repeated extrect calculations and simplify root ordering per tile.
2026-01-15 12:53:21 +01:00
Alejandro Alonso afc914f486 🎉 Render simple shapes directly on Current
Bypass intermediate surfaces for simple shapes without effects.
2026-01-15 12:53:21 +01:00
Pablo Alba 0d5fe6e527 🐛 Fix wrong register image 2026-01-15 11:25:12 +01:00
Pablo Alba e7230d9da4 🐛 Fix wrong image in the onboarding invitation block 2026-01-15 11:25:12 +01:00
Elena Torro 5054f6bc38 🔧 Optimize sidebar performance for deeply nested shapes
- Batch hover highlights using RAF to avoid long tasks from rapid events
- Run parent expansion asynchronously to not block selection
- Lazy-load children in layer items using IntersectionObserver
- Clarify expand-all-parents logic with explicit bindings
2026-01-15 09:15:32 +01:00
andrés gonzález b72de2dc8f 📚 Add shadow token documentation (#8082) 2026-01-15 09:09:08 +01:00
Alejandro Alonso 84f750da0d 🎉 Skip heavy effects in fast mode
Avoid blur and shadow passes for text and shapes when FAST_MODE is enabled.
2026-01-15 08:45:21 +01:00
Xaviju 38396ba299 ♻️ Divide token integrations tests into multiple files (#8076) 2026-01-14 16:12:36 +01:00
Pablo Alba b1997a83b3 🐛 Fix prototype connections lost when switching between variants 2026-01-14 15:30:54 +01:00
Elena Torro 68f5671eab 🔧 Always lookup over a set 2026-01-14 13:49:32 +01:00
Elena Torro 92976143bb 🔧 Add performance debugging logs 2026-01-14 13:49:32 +01:00
Eva Marco dd2d03e6a0 ♻️ Replace border radius inputs (#7953)
*  Replace border radius numeric input

*  Add border radius token inputs on multiple selection
2026-01-14 12:45:40 +01:00
Elena Torro fd675e0194 🔧 Lookup page objects only when value changes 2026-01-14 12:15:01 +01:00
Elena Torro a3119bef5e 🔧 Show message and button to reload the page when WebGL context is lost 2026-01-14 11:10:03 +01:00
Alejandro Alonso c60d74df62 🐛 Fix nested frames border clipping 2026-01-14 11:10:03 +01:00
Alejandro Alonso d593e299e3 🐛 Fix mask erros on save/restore optimizations 2026-01-14 11:10:03 +01:00
Alejandro Alonso 4a8e02987f 🐛 Fix mask erros on save/restore optimizations 2026-01-14 11:10:03 +01:00
Alejandro Alonso ee766e85a0 🎉 Wasm render dirty surfaces 2026-01-14 11:10:03 +01:00
Alejandro Alonso 35e3b7f19a 🎉 Root ids refactor 2026-01-14 11:10:03 +01:00
Alejandro Alonso 1810df232b 🎉 Ignore frames and groups when they have no visual extra information 2026-01-14 11:10:03 +01:00
Alejandro Alonso 3e99ad036c 🎉 Avoid unnecesary saves and restores 2026-01-14 11:10:03 +01:00
Alejandro Alonso 042a3a4080 🐛 Fix wasm playgrounds 2026-01-14 11:10:03 +01:00
Belén Albeza f0687fd1f7 🎉 Make workspace loader to wait for first render 2026-01-14 11:10:03 +01:00
Aitor Moreno 2c9159288f 🐛 Fix previous styles lost when changing selected text 2026-01-14 11:10:01 +01:00
Alejandro Alonso 51635770ce Merge pull request #8065 from penpot/eva-fix-lost-translation
🐛 Fix translation on rename token
2026-01-14 09:44:41 +01:00
Eva Marco 18a4e63da0 🐛 Fix translation on rename token 2026-01-13 15:57:09 +01:00
Andrey Antukh c98373658e Revert "🔧 Use selfhosted runners (#8057)"
This reverts commit 01e42b0458.
2026-01-13 15:45:28 +01:00
Andrey Antukh 2e400768b7 Add the ability to log using logging subsystem the audit events 2026-01-13 14:22:18 +01:00
David Barragán Merino 01e42b0458 🔧 Use selfhosted runners (#8057) 2026-01-13 14:20:33 +01:00
Madalena Melo 7529673812 📎 Create an issue template for reporting bugs on the new render engine (#8059)
This template will be part of the open beta test for the new render, so that users can report issues for us to review.
2026-01-13 13:27:27 +01:00
Andrey Antukh d2dad35d7a Merge branch 'staging' into develop 2026-01-13 10:36:22 +01:00
Andrey Antukh d71f811dbe Update help center build script 2026-01-13 10:35:15 +01:00
Eva Marco f7b5266304 🐛 Fix sticky-buttons-on-layers (#7962) 2026-01-12 14:28:19 +01:00
Eva Marco 09c23256b7 🐛 Fix multiselection with tokens on stroke color (#7977) 2026-01-12 12:54:24 +01:00
Andrey Antukh 1ae1c0460e 🐛 Fix translation related to dashboard deleted page (#8056)
* 🐛 Fix translation related to dashboard deleted page

*  Rehash and validate translation files
2026-01-12 12:42:51 +01:00
Andrey Antukh 291c7349db Merge remote-tracking branch 'origin/staging' into develop 2026-01-12 11:53:02 +01:00
Andrey Antukh 1beb3b86aa 🐛 Add missing msgid_plural on several translations 2026-01-12 11:52:07 +01:00
Andrey Antukh b605a3b53d Merge branch 'staging' into develop 2026-01-12 11:44:51 +01:00
Andrey Antukh fe20bdd00e 🐛 Fix multiple selection options on dashboard deleted page (#8055)
* 🐛 Fix multiple selection options on dashboard deleted page

* 📎 Fix translations
2026-01-12 11:41:37 +01:00
María Valderrama 5420897b92 🐛 Fix empty state message in trash page (#8045) 2026-01-12 11:17:01 +01:00
Andrey Antukh e430a4c9f3 Revert " Backport translations from develop"
This reverts commit ec6d72bd91.
2026-01-12 10:40:17 +01:00
Andrey Antukh ec6d72bd91 Backport translations from develop 2026-01-12 09:39:33 +01:00
Nicola Bortoletto cc2dab2756 🌐 Add translations for: Italian
Currently translated at 98.4% (2032 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-01-12 09:36:34 +01:00
Alexis Morin d0c0664338 🌐 Add translations for: French (Canada)
Currently translated at 16.5% (342 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-12 09:36:34 +01:00
Alexis Morin 2240f25143 🌐 Add translations for: French (Canada)
Currently translated at 15.3% (317 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-12 09:36:34 +01:00
Oğuz Ersen 93a5ec2f5d 🌐 Add translations for: Turkish
Currently translated at 99.8% (2061 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-01-12 09:36:33 +01:00
Alexis Morin d6784771a8 🌐 Add translations for: French (Canada)
Currently translated at 13.5% (279 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-12 09:36:33 +01:00
Edgars Andersons 930c814ded 🌐 Add translations for: Latvian
Currently translated at 91.1% (1883 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-01-12 09:36:33 +01:00
Stephan Paternotte 1a5a69bca2 🌐 Add translations for: Dutch
Currently translated at 99.8% (2061 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-01-12 09:36:33 +01:00
VKing9 9ad323a220 🌐 Add translations for: Hindi
Currently translated at 96.9% (2001 of 2065 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-01-12 09:36:33 +01:00
Andrey Antukh bcaf76d055 Make the virtual clock by profile and not global
And make it affect only RPC/HTTP requests and not worker
tasks.
2026-01-09 12:58:44 +01:00
Eva Marco 6ec451b46d 🐛 Fix resolved value on line height (#8047) 2026-01-09 12:54:24 +01:00
Andrés Moya 5fa4368d70 🔧 Refactor integration test to be cleaner (#8044) 2026-01-09 12:52:50 +01:00
Elena Torró b8efd2518d 🐛 Fix invite members UI modal (#8032) 2026-01-09 11:12:15 +01:00
Andrey Antukh 2b836f10cb 🐛 Do not show deleted files on search (#8036)
* 🐛 Do not show deleted files on search

* 💄 Add cosmetic changes to dashboard deleted files page
2026-01-09 11:11:29 +01:00
Andrés Moya 7b2271ec38 🐛 Fix remapping of tokens with the same name and update tests (#8043) 2026-01-09 10:53:19 +01:00
Xaviju 2240d93069 Save unfolded tokens path (#7949) 2026-01-09 09:56:18 +01:00
Edgars Andersons 3f4506284b 🌐 Add translations for: Latvian
Currently translated at 91.3% (1869 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-01-08 18:06:51 +01:00
Valentina Chapellu af1dfd91aa 🌐 Add translations for: Italian
Currently translated at 97.0% (1984 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-01-08 18:06:51 +01:00
Mikel Larreategi 24feebd73b 🌐 Add translations for: Basque
Currently translated at 56.4% (1155 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2026-01-08 18:06:51 +01:00
Aryiu 33e5a9a538 🌐 Add translations for: Catalan
Currently translated at 52.2% (1068 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2026-01-08 18:06:50 +01:00
Linerly 9c69b07a62 🌐 Add translations for: Indonesian
Currently translated at 82.9% (1697 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2026-01-08 18:06:50 +01:00
Црнобог 56f5be4f37 🌐 Add translations for: Serbian
Currently translated at 67.0% (1371 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2026-01-08 18:06:50 +01:00
ascarida 8a70204d41 🌐 Add translations for: Galician
Currently translated at 18.0% (370 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/gl/
2026-01-08 18:06:50 +01:00
Henrik Allberg 57a27f7e7f 🌐 Add translations for: Swedish
Currently translated at 97.1% (1986 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-01-08 18:06:50 +01:00
Eranot 3b0b2a78d6 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 68.1% (1394 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-01-08 18:06:50 +01:00
Alejandro Alonso 10bf4610df 🌐 Add translations for: Hausa
Currently translated at 60.6% (1241 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2026-01-08 18:06:50 +01:00
Andy Li 77e8414aea 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 78.1% (1599 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-01-08 18:06:50 +01:00
bingling_sama 20ecf3b066 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 88.2% (1804 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-01-08 18:06:50 +01:00
Amerey.eu 49b1032973 🌐 Add translations for: Czech
Currently translated at 77.9% (1594 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2026-01-08 18:06:49 +01:00
Radek Sawicki 5ba7dd8c56 🌐 Add translations for: Polish
Currently translated at 55.2% (1130 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2026-01-08 18:06:49 +01:00
Ingrid Pigueron 38b5125186 🌐 Add translations for: French
Currently translated at 94.5% (1934 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-01-08 18:06:49 +01:00
Vint Prox 6677ae83d4 🌐 Add translations for: Russian
Currently translated at 77.3% (1582 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-01-08 18:06:49 +01:00
Marius 0737c055f0 🌐 Add translations for: German
Currently translated at 93.2% (1906 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-01-08 18:06:49 +01:00
Dário 4b88748fe3 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 76.8% (1571 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-01-08 18:06:49 +01:00
Denys Kisil 92107e5b1e 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 88.8% (1818 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-01-08 18:06:49 +01:00
Shuaib Zahda ebc0e3a23c 🌐 Add translations for: Arabic
Currently translated at 55.0% (1126 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-01-08 18:06:49 +01:00
VKing9 ebe4f2da50 🌐 Add translations for: Hindi
Currently translated at 97.1% (1986 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-01-08 18:06:48 +01:00
Vincas Dundzys a07c1d6eaa 🌐 Add translations for: Lithuanian
Currently translated at 5.7% (118 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lt/
2026-01-08 18:06:48 +01:00
Ahmad HosseinBor 613bfda955 🌐 Add translations for: Persian
Currently translated at 38.2% (782 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2026-01-08 18:06:48 +01:00
AlexTECPlayz f7ef6618e5 🌐 Add translations for: Romanian
Currently translated at 94.8% (1940 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-01-08 18:06:48 +01:00
Sebastiaan Pasma fe334d9cbe 🌐 Add translations for: Dutch
Currently translated at 97.1% (1986 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-01-08 18:06:48 +01:00
Revenant 268b883c73 🌐 Add translations for: Malay
Currently translated at 32.8% (672 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2026-01-08 18:06:48 +01:00
Zvonimir Juranko f6a4effa29 🌐 Add translations for: Croatian
Currently translated at 78.1% (1599 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-01-08 18:06:48 +01:00
Yessenia Villarte Vaca ced848077e 🌐 Add translations for: Spanish (Latin America)
Currently translated at 6.4% (131 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es_419/
2026-01-08 18:06:48 +01:00
Alexis Morin 7d9d318539 🌐 Add translations for: French (Canada)
Currently translated at 12.5% (257 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 18:06:48 +01:00
Oğuz Ersen 9781fceadb 🌐 Add translations for: Turkish
Currently translated at 97.1% (1986 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-01-08 18:06:48 +01:00
Yaron Shahrabani 3178bd9a27 🌐 Add translations for: Hebrew
Currently translated at 97.0% (1984 of 2045 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-01-08 18:06:47 +01:00
Andrey Antukh e5d677f449 🌐 Validate and rehash translation files 2026-01-08 18:05:51 +01:00
Andrey Antukh 6bf928893c Merge pull request #8000 from penpot/luis-radio-buttons-ds
♻️ Replace some components with DS ones
2026-01-08 18:04:20 +01:00
Andrey Antukh 1ae0f3fc87 Merge pull request #8037 from penpot/niwinz-staging-project-name-fix
🐛 Fix long project name visual problem on dashboard
2026-01-08 17:41:37 +01:00
Andrés Moya 53dd90aa24 🔥 Remove unused css (#8039) 2026-01-08 16:37:27 +01:00
Eva Marco e13c203b8d ♻️ Refactor scss file 2026-01-08 16:35:56 +01:00
Andrey Antukh 9fd0f6a8f3 📎 Fix integration tests 2026-01-08 16:02:52 +01:00
Andrey Antukh 638c3356d3 📎 Use correct casing on translation strings 2026-01-08 14:58:17 +01:00
Luis de Dios 6879f54e5d ♻️ Replace some components with DS ones 2026-01-08 14:52:25 +01:00
Andrey Antukh a71baa5a78 🌐 Rehash and validate translation files 2026-01-08 14:46:18 +01:00
Andrey Antukh 8e4a89bd1c Merge branch 'staging' into develop 2026-01-08 14:43:43 +01:00
Andrey Antukh 90efb665b5 Add several additional renames for make translation string consistent 2026-01-08 14:37:58 +01:00
Pablo Alba 47ee490158 🐛 Fix typos on download modal 2026-01-08 14:37:58 +01:00
Andrey Antukh f0f89599bc 🌐 Backport translations from develop 2026-01-08 14:08:02 +01:00
Hosted Weblate 7aad9da285 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-01-08 14:04:56 +01:00
Alexis Morin ab57a4ae52 🌐 Add translations for: French (Canada)
Currently translated at 12.9% (259 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 14:04:48 +01:00
Alexis Morin 266ee29bb9 🌐 Add translations for: French (Canada)
Currently translated at 9.2% (184 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 14:04:48 +01:00
Alexis Morin 69ca86bb6c 🌐 Add translations for: French (Canada)
Currently translated at 7.3% (147 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 14:04:48 +01:00
Alexis Morin ee14a845fc 🌐 Add translations for: French (Canada)
Currently translated at 3.1% (62 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 14:04:48 +01:00
Yaron Shahrabani 73639f5d16 🌐 Add translations for: Hebrew
Currently translated at 99.7% (1992 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-01-08 14:04:48 +01:00
Yaron Shahrabani 9bd106b2bc 🌐 Add translations for: Hebrew
Currently translated at 99.4% (1986 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-01-08 14:04:47 +01:00
Alexis Morin 59c75afc7b 🌐 Add translations for: French (Canada)
Currently translated at 1.0% (21 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 14:04:47 +01:00
Nicola Bortoletto bbc81586e3 🌐 Add translations for: Italian
Currently translated at 99.7% (1992 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-01-08 14:04:47 +01:00
Anton Palmqvist c9c30eab75 🌐 Add translations for: Swedish
Currently translated at 99.8% (1994 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-01-08 14:04:47 +01:00
Alexis Morin 86ba9280db 🌐 Add translations for: French (Canada)
Currently translated at 0.3% (6 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-01-08 14:04:47 +01:00
Vin 5800cc4bb2 🌐 Add translations for: Russian
Currently translated at 79.2% (1583 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-01-08 14:04:47 +01:00
andy aa29a34c4c 🌐 Added translation for: French (Canada) 2026-01-08 14:04:47 +01:00
Edgars Andersons 3276129cc7 🌐 Add translations for: Latvian
Currently translated at 93.9% (1876 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-01-08 14:04:47 +01:00
VKing9 67a96de475 🌐 Add translations for: Hindi
Currently translated at 100.0% (1997 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-01-08 14:04:47 +01:00
Stephan Paternotte 48785b4846 🌐 Add translations for: Dutch
Currently translated at 99.8% (1994 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-01-08 14:04:46 +01:00
Oğuz Ersen 3f0573f95d 🌐 Add translations for: Turkish
Currently translated at 99.8% (1994 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-01-08 14:04:46 +01:00
Andrey Antukh d94a2a8881 Merge branch 'staging-render' into develop 2026-01-08 13:59:01 +01:00
Andrey Antukh 1c237a0968 Merge branch 'staging' into staging-render 2026-01-08 13:58:48 +01:00
Andrey Antukh b0dc7d6ffb 🔧 Change default jmx port on deps.edn 2026-01-08 13:56:22 +01:00
Elena Torró b7eaeffa88 Merge pull request #8024 from penpot/azazeln28-issue-12835-fix-previous-styles-lost
🐛 Fix previous styles lost when changing selected text
2026-01-08 13:49:06 +01:00
Andrey Antukh 722fcc1f82 Merge remote-tracking branch 'origin/staging' into develop 2026-01-08 13:48:21 +01:00
Andrey Antukh b7cd315872 🐛 Fix wasm-playground on devenv 2026-01-08 13:48:09 +01:00
Andrés MoyaandAkshay Gupta 2ad42cfd9b Add ability to remap tokens when renamed ones are referenced by other child tokens (#8035)
* 🎉 Add ability to remap tokens when renamed ones are referenced by other child tokens

Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>

* 🐛 Fix remap skipping tokens with same name in different sets

* 📚 Update CHANGES.md

* 🔧 Fix css styles

---------

Signed-off-by: Akshay Gupta <gravity.akshay@gmail.com>
Co-authored-by: Akshay Gupta <gravity.akshay@gmail.com>
2026-01-08 13:42:06 +01:00
Eva Marco 743d4e5c8d 🐛 Fix error on shadow token creation (#8029) 2026-01-08 13:26:01 +01:00
Andrey Antukh 97e4f4c424 🐛 Fix long project name visual problem on dashboard 2026-01-08 11:40:55 +01:00
Belén Albeza fb9560c315 🐛 Fix guides dropdown width (#8031)
* 🐛 Fix width of guides column dropdown

* ♻️ Remove deprecated tokens in css

* 🔧 Update changelog
2026-01-08 10:47:11 +01:00
Andrey Antukh 795f65632a 🐛 Fix wasm-playground on devenv 2026-01-08 10:42:37 +01:00
Alejandro Alonso d53c090900 Merge pull request #8028 from penpot/elenatorro-12956-fix-text-color-tokens
🐛 Fix missing text color token from selected shapes in selected colors list
2026-01-07 16:49:41 +01:00
Elena Torro 621e030095 🐛 Fix missing text color token from selected shapes in selected colors list 2026-01-07 16:41:25 +01:00
Alejandro Alonso 157e4aa2d0 Merge pull request #8025 from penpot/elenatorro-12951-fix-inner-text-shadow-token
🐛 Fix inner shadow selector on shadow token
2026-01-07 16:37:19 +01:00
Elena Torro 7cd2308f3b 🐛 Fix inner shadow selector on shadow token 2026-01-07 16:36:51 +01:00
Alejandro Alonso c315a15b48 Merge pull request #8026 from penpot/elenatorro-12997-fix-clojure-on-css-box-shadow
🐛 Fix CSS generated box-shadow property
2026-01-07 16:32:12 +01:00
Elena Torro 8a3e6d026e 🐛 Fix CSS generated box-shadow property 2026-01-07 16:28:05 +01:00
Florian Schrödl 0dd062d011 🐛 Fix line-height throwing for int (#7927) 2026-01-07 16:13:10 +01:00
Alejandro Alonso bfbb546699 Merge pull request #8027 from penpot/superalex-fix-colors-assets-from-shared-libraries
🐛 Fix color assets from shared libraries
2026-01-07 14:16:57 +01:00
Alejandro Alonso 083e77e9c5 🐛 Fix color assets from shared libraries 2026-01-07 14:02:28 +01:00
Aitor Moreno 7819e6c440 🐛 Fix previous styles lost when changing selected text 2026-01-07 12:41:39 +01:00
Andrey Antukh 952f622ce9 🔧 Add 'Reapply` prefix to valid commit checker prefixes 2026-01-07 11:56:38 +01:00
Andrey Antukh a6c6f97f47 Reapply "💄 Group tokens by name path (#7775)"
This reverts commit eff572d3bb.
2026-01-07 11:55:56 +01:00
Andrey Antukh 88424eb54a Merge branch 'staging' into develop 2026-01-07 11:55:40 +01:00
Alejandro Alonso 919f78daeb Merge pull request #7965 from penpot/eva-fix-styles-on-viewer
🐛 Fix inspect tab styles on viewer
2026-01-07 11:54:33 +01:00
Eva Marco b5c30f8c41 🐛 Fix inspect tab styles on viewer 2026-01-07 11:41:49 +01:00
Alejandro Alonso 60aa426753 Merge pull request #8022 from penpot/alotor-fix-drag-handlers
🐛 Fix problem with dragging handlers
2026-01-07 11:36:26 +01:00
Alejandro Alonso 86f7d6b26b Sanitizing error values (#8020) 2026-01-07 11:23:19 +01:00
Andrey Antukh 36732a4bd3 Make the devenv runtine initialization yarn independent (#8023) 2026-01-07 11:21:58 +01:00
Andrey Antukh eff572d3bb Revert "💄 Group tokens by name path (#7775)"
This reverts commit 0956b66281.
2026-01-07 11:20:44 +01:00
alonso.torres d470d96833 🐛 Fix problem with dragging handlers 2026-01-07 11:00:02 +01:00
Aitor Moreno cab70773d2 Merge pull request #7667 from penpot/azazeln28-doc-add-more-info-text-editor-v2-readme
📚 Add more info about text editor v2
2026-01-07 09:40:06 +01:00
Alejandro Alonso de9a21121a Merge remote-tracking branch 'origin/staging' into develop 2026-01-05 13:22:14 +01:00
Alejandro Alonso 32ca42a093 Merge remote-tracking branch 'origin/staging-render' into staging 2026-01-05 13:21:58 +01:00
Alejandro Alonso 523a97a4ec Merge pull request #8016 from penpot/alotor-fix-refresh-thumbnails
🐛 Fix problem with thumbnail regeneration
2026-01-05 13:21:34 +01:00
Alejandro Alonso 260f6861a3 Merge pull request #8015 from penpot/alotor-fix-grid-component-auto-sizing
🐛 Fix problem with grid layout components and auto sizing
2026-01-05 13:18:59 +01:00
alonso.torres edd53b419a 🐛 Fix problem with thumbnail regeneration 2026-01-05 13:09:40 +01:00
Alejandro Alonso cea10308b7 Merge remote-tracking branch 'origin/staging' into develop 2026-01-05 11:52:15 +01:00
alonso.torres 078a3d5a5c 🐛 Fix problem with grid layout components and auto sizing 2026-01-05 10:54:36 +01:00
Alejandro Alonso c4e57427ac Merge branch 'staging-render' into staging 2026-01-05 10:30:06 +01:00
David Barragán Merino 5223c9c881 🔧 Fix a typo in an interpolation 2026-01-05 09:13:14 +01:00
Alejandro Alonso be62fa10c4 📎 Bump new version on changelog 2026-01-05 08:42:57 +01:00
Alejandro Alonso 7a6405481c Merge remote-tracking branch 'origin/develop' into staging 2026-01-05 08:37:19 +01:00
Alejandro Alonso 218f34380a Merge pull request #8012 from penpot/azazeln28-refactor-minor-changes
♻️ Minor naming changes and event handling
2026-01-02 14:16:55 +01:00
Alejandro Alonso 47aaa2b5fa Merge pull request #8011 from penpot/alotor-fix-trash-bar
🐛 Fix problems with trash bar in dashboard
2026-01-02 13:46:53 +01:00
Aitor Moreno 6c6b3db87e ♻️ Minor naming changes and event handling 2026-01-02 13:41:48 +01:00
Alejandro Alonso 6eb32cfb79 Merge pull request #8008 from penpot/alotor-fix-path-editor
🐛 Fix problem with path editor and right click
2026-01-02 13:29:44 +01:00
alonso.torres dbba3496af 🐛 Fix problems with trash bar in dashboard 2026-01-02 12:00:16 +01:00
alonso.torres 55752d361f 🐛 Fix problem with path editor and right click 2026-01-02 10:37:52 +01:00
Alejandro Alonso fe94ee4526 Merge pull request #8009 from penpot/alotor-fix-style-font-input
🐛 Fix problem with style in fonts input
2026-01-02 10:23:02 +01:00
Aitor Moreno e39f292499 📚 Add more info about text editor v2 2026-01-02 10:13:34 +01:00
Andrey Antukh 5c71c57dd9 Merge tag '2.12.1' 2025-12-30 15:37:30 +01:00
Andrey Antukh 52b8560b70 Merge branch 'staging-render' into develop 2025-12-30 15:30:56 +01:00
Andrey Antukh 75860afe57 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-30 15:29:58 +01:00
Andrey Antukh 824ca1bbca 🔧 Make devenv init yarn indpendent 2025-12-30 15:28:19 +01:00
Andrey Antukh 5b6f9c1741 📎 Disable legacy cache on plugins nx config 2025-12-30 14:56:15 +01:00
Andrey Antukh 19853b832b 📚 Update documentation 2025-12-30 14:56:15 +01:00
Andrey Antukh d20c011db2 Migrate plugins to pnpm 2025-12-30 14:56:15 +01:00
Andrey Antukh 9431ae6858 📎 Update docs 2025-12-30 14:56:15 +01:00
Andrey Antukh 96356c1b89 ⬆️ Update storybook and fix compatibility issues 2025-12-30 14:56:15 +01:00
Andrey Antukh b7b68eeb47 🔥 Remove npx prefix on package.json scripts 2025-12-30 14:56:15 +01:00
Andrey Antukh 9bbeb657f8 🔧 Add plugins runtime ci job 2025-12-30 14:56:15 +01:00
Andrey Antukh ec1af4ad96 🎉 Import penpot-plugins repository
As commit 819a549e4928d2b1fa98e52bee82d59aec0f70d8
2025-12-30 14:56:15 +01:00
alonso.torres 23e7116b24 🐛 Fix problem with style in fonts input 2025-12-30 14:28:10 +01:00
Alejandro Alonso 48e3f35bb3 🐛 Fix setting a portion of text as bold or underline messes things up 2025-12-30 11:34:24 +01:00
Andrey Antukh 6b794c9d12 Merge branch 'staging' into staging-render 2025-12-30 11:13:15 +01:00
Yamila Moreno d3ee50daf5 🔧 Add ci for branch staging-render 2025-12-30 11:13:00 +01:00
Yamila Moreno 22a36d59d8 🔧 Add ci for branch staging-render 2025-12-30 10:57:51 +01:00
Alejandro Alonso a948e49e51 🐛 Fix using cache on first zoom after pan 2025-12-30 10:03:24 +01:00
Alejandro Alonso d635f5a8dc 🐛 Detecting situations where WebGL context is lost or no WebGL support 2025-12-30 10:03:24 +01:00
Alejandro Alonso ab3a3ef43b 🎉 Resize cache only when required 2025-12-30 10:03:24 +01:00
Alejandro Alonso 9c21fd3359 🐛 Fix resize cache memory leak 2025-12-30 10:03:24 +01:00
Andrey Antukh 7b5817f407 ♻️ Make several adjustments to the dashboard deleted page (#7999)
* ♻️ Make several sustantial adjustments to the dashboard deleted page

* 📎 Add PR feedback changes
2025-12-30 09:52:29 +01:00
Yamila Moreno e3405eacca 🔧 Improve mattermost notification 2025-12-29 19:06:26 +01:00
Alejandro Alonso 44b70cf1d4 Merge pull request #7998 from penpot/alotor-fix-problem-with-create-grid
🐛 Fix problem creating grid from elements
2025-12-29 14:31:15 +01:00
Alejandro Alonso a8bd74b392 Merge pull request #8001 from penpot/alotor-fix-gfonts-references
🐛 Fix problem with some fonts
2025-12-29 14:25:54 +01:00
alonso.torres 3d3e3582d6 🐛 Fix problem with some fonts 2025-12-29 12:35:19 +01:00
Andrey Antukh de052b5161 📎 Update changelog 2025-12-29 11:10:04 +01:00
Andrey Antukh e01654ba43 Merge branch 'staging-render' into develop 2025-12-29 10:43:00 +01:00
Andrey Antukh 6ebd48b94c Merge branch 'staging' into staging-render 2025-12-29 10:41:08 +01:00
Andrey Antukh 8a3b33797f 🐛 Fix error handling on password change form
Fixes https://github.com/penpot/penpot/issues/7978
2025-12-29 10:27:27 +01:00
Andrey Antukh 13fd20f76f Backport form error management improvements from develop 2025-12-29 10:27:27 +01:00
alonso.torres 417cd80564 🐛 Fix problem creating grid from elements 2025-12-23 14:49:21 +01:00
Alejandro Alonso a57011ec7b Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-23 13:35:27 +01:00
Andrey Antukh 69c880d00e 🐛 Fix importmap usage on firefox 2025-12-23 13:10:58 +01:00
Andrey Antukh 9eebc467ef Preload default translations 2025-12-23 13:10:58 +01:00
Andrey Antukh b77712ce73 Move frontend/vendor to frontend/packages 2025-12-23 13:10:58 +01:00
Andrey Antukh 3d3e81f314 Replace tubax with more modern tooling 2025-12-23 13:10:58 +01:00
Andrey Antukh fe6441bb24 Replace hightlight.js internal bundle with direct npm use 2025-12-23 13:10:58 +01:00
Andrey Antukh e15f0baf30 Replace direct draft-js usage with internal module 2025-12-23 13:10:58 +01:00
Andrey Antukh c040cbb784 🔥 Remove old gulp related dependencies 2025-12-23 13:10:58 +01:00
Andrey Antukh 7f674b78a9 📎 Move all deps to dev-dependencies on frontend package.json
All they only needed for build process.
2025-12-23 13:10:58 +01:00
Andrey Antukh 099b78affd 📎 Update frontend yarn.lock file 2025-12-23 13:10:58 +01:00
Andrey Antukh 78cc3f0aa4 📎 Add immutable dependency to vendor/draft-js 2025-12-23 13:10:58 +01:00
Andrey Antukh 76f5f12808 ⬆️ Update dependencies on exporter 2025-12-23 13:10:58 +01:00
Alejandro Alonso cb325282ec Merge pull request #7994 from penpot/alotor-fix-font-style
🐛 Fix problem when changing colors with multiple fonts
2025-12-23 07:34:41 +01:00
Andrey Antukh 01ecde3bfa Add the ability to add relations on penpot sdk (#7987)
*  Add the ability to add relations on penpot sdk

* 📎 Remove debug console log
2025-12-22 20:55:31 +01:00
Andrey Antukh 047483a70a 🐛 Fix deleted files thumbnails generation 2025-12-22 20:20:43 +01:00
Alonso Torres 4000ec8762 🐛 Fix problem resizing auto size layouts (#7995) 2025-12-22 20:17:11 +01:00
Andrey Antukh 8cb2f27de8 ♻️ Move file permissions to binfile common ns 2025-12-22 20:16:41 +01:00
Andrey Antukh 0433336fc9 📎 Use correct criterium version on frontend deps 2025-12-22 20:16:41 +01:00
Andrey Antukh ce234fbeda Allow get thumbnails for deleted files 2025-12-22 20:16:41 +01:00
Andrey Antukh fc4d31eed7 Add minor efficiency improvements to deleted dashboard page 2025-12-22 20:16:41 +01:00
María Valderrama c670aac339 🎉 Added deleted files to dashboard 2025-12-22 20:16:41 +01:00
Andrés Moya 1d3fb5434f Enable shadow tokens by default (#7996) 2025-12-22 18:17:29 +01:00
Andrey Antukh f478399ae0 Merge remote-tracking branch 'origin/staging-render' into develop 2025-12-22 17:28:18 +01:00
Andrey Antukh 6a1854f180 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-22 17:28:01 +01:00
Andrés Moya 0858e297e5 🎉 Add composite tokens to plugins API (#7992) 2025-12-22 17:14:54 +01:00
alonso.torres bd580ab159 🐛 Fix problem when changing colors with multiple fonts 2025-12-22 17:14:37 +01:00
Alejandro Alonso 5780a43fe0 🐛 Fix object added in different page (#7988) 2025-12-22 16:59:47 +01:00
Alejandro Alonso 737eceda3a 🐛 Fix unmasking shapes (#7989) 2025-12-22 16:59:04 +01:00
Alonso Torres 923c3c2dbd 🐛 Fix font weight token (#7991) 2025-12-22 16:58:26 +01:00
Alejandro Alonso a14b4561e7 🐛 Fix comment bubbles (#7990) 2025-12-22 16:57:45 +01:00
Andrey Antukh bb5568e15a 🎉 Enable hindi translations on the application 2025-12-22 16:57:00 +01:00
Pablo Alba 5cbcec3db6 🐛 Fix "maximum call stack size exceeded" crash on variant 2025-12-22 16:57:00 +01:00
Anonymous 105e1fe86c 🌐 Add translations for: Spanish
Currently translated at 97.1% (1940 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2025-12-22 16:34:43 +01:00
Yaron Shahrabani 3e0a916883 🌐 Add translations for: Hebrew
Currently translated at 99.3% (1985 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-12-22 16:34:42 +01:00
Ahmad HosseinBor 4f80238bc2 🌐 Add translations for: Persian
Currently translated at 39.2% (783 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2025-12-22 16:34:42 +01:00
Alejandro Alonso 5156cc5d9a 🌐 Add translations for: Yoruba
Currently translated at 58.8% (1176 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2025-12-22 16:34:42 +01:00
Yessenia Villarte Vaca 42c46b6cfc 🌐 Add translations for: Spanish (Latin America)
Currently translated at 6.5% (131 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es_419/
2025-12-22 16:34:41 +01:00
VKing9 8b3c40b35e 🌐 Add translations for: Hindi
Currently translated at 99.6% (1991 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2025-12-22 16:34:41 +01:00
Andy Li d3996e5fb1 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 80.1% (1601 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2025-12-22 16:34:40 +01:00
Anonymous 0c42bca866 🌐 Add translations for: German
Currently translated at 95.5% (1908 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-22 16:34:40 +01:00
Marius e5685c1f1c 🌐 Add translations for: German
Currently translated at 95.5% (1908 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-22 16:34:40 +01:00
Anonymous 2784209bde 🌐 Add translations for: Turkish
Currently translated at 99.5% (1989 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-12-22 16:34:40 +01:00
Alejandro Alonso 024f460e99 🌐 Add translations for: Igbo
Currently translated at 25.6% (512 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2025-12-22 16:34:39 +01:00
Anonymous 1d9b76b62a 🌐 Add translations for: Romanian
Currently translated at 97.2% (1942 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2025-12-22 16:34:39 +01:00
Shuaib Zahda 7e17a75b7d 🌐 Add translations for: Arabic
Currently translated at 56.4% (1127 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2025-12-22 16:34:39 +01:00
Anonymous ca093d6fae 🌐 Add translations for: French
Currently translated at 96.8% (1934 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-22 16:34:38 +01:00
Alexandre Pawlak 0f0b7562b5 🌐 Add translations for: French
Currently translated at 96.8% (1934 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-22 16:34:38 +01:00
Anonymous 9cdc694697 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 90.3% (1804 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2025-12-22 16:34:37 +01:00
Dário b972a4033b 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 90.3% (1804 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2025-12-22 16:34:37 +01:00
Anonymous cbe9f4da51 🌐 Add translations for: Swedish
Currently translated at 99.3% (1985 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-12-22 16:34:37 +01:00
Anonymous c583bde9e3 🌐 Add translations for: Russian
Currently translated at 76.8% (1534 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-12-22 16:34:37 +01:00
Vint Prox 3911ebdc4e 🌐 Add translations for: Russian
Currently translated at 76.8% (1534 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2025-12-22 16:34:36 +01:00
Anonymous 3e3b18667b 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 69.9% (1396 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2025-12-22 16:34:36 +01:00
Anonymous ed81c9b8df 🌐 Add translations for: Dutch
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-22 16:34:36 +01:00
Sebastiaan Pasma fbdf98d29c 🌐 Add translations for: Dutch
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-22 16:34:36 +01:00
Anonymous e603825a55 🌐 Add translations for: Italian
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-22 16:34:35 +01:00
Valentina Chapellu 1d724783e6 🌐 Add translations for: Italian
Currently translated at 99.5% (1988 of 1997 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-22 16:34:35 +01:00
Hosted Weblate e0abe7dcb5 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2025-12-22 16:33:05 +01:00
Andrey Antukh 5c1bbf5be8 Merge remote-tracking branch 'weblate/develop' into develop 2025-12-22 16:32:30 +01:00
Pablo Alba bbb0d58190 🐛 Fix "maximum call stack size exceeded" crash on variant 2025-12-22 16:27:10 +01:00
Andrey Antukh 88dcf9d1fe 🐛 Mark rpc calls as authenticated when shared key is used (#7901) 2025-12-22 12:18:36 +01:00
Alejandro Alonso fe44c14bac Merge pull request #7982 from penpot/niwinz-staging-import-bucket
🐛 Prefill storage object bucket if it comes nil on import binfile
2025-12-22 12:17:16 +01:00
Belén Albeza 20061067ad 🐛 Fix text editor not getting focus back after font variant change 2025-12-22 11:18:25 +01:00
Andrey Antukh 336173645e 🐛 Fix regression on export shape on plungins API 2025-12-22 10:41:42 +01:00
Andrey Antukh 2acf15958b Merge branch 'staging-render' into develop 2025-12-22 09:24:04 +01:00
Andrey Antukh 08267de242 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-22 09:23:48 +01:00
Pablo Alba 35fb376a78 Add proxypass to caddyfile on devenv (#7985) 2025-12-22 09:21:22 +01:00
Dalai FelintoandDalai Felinto 13fcf3a9bb 💄 Set import Tokens default option to be Single JSON value (#7918)
This patches makes the default Tokens importing option to match the
current default Tokens exporting option (single JSON value). This way it
is more obvious and quick to export the tokens from a file and import
in new one,

---

While testing our design system we are often re-exporting and
re-importing the Tokens to the files using the design system components.

I'm aware that this may be addressed in the future so the Tokens are
brought in together with the library. Meanwhile (and even in the future)
I think it is sensible to have a symmetry between the export and import
defeault options.

Co-authored-by: Dalai Felinto <dalai@blender.org>
2025-12-19 10:44:05 +01:00
Andrey Antukh 83bb4bf221 🐛 Prefill storage object bucket if it comes nil on import binfile 2025-12-19 09:32:51 +01:00
Henrik Steffens dba6ae2820 🌐 Add translations for: German
Currently translated at 95.8% (1911 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-17 13:00:25 +01:00
Marius ada101c236 🌐 Add translations for: German
Currently translated at 95.8% (1911 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-17 13:00:24 +01:00
Marius ea48fb5825 🌐 Add translations for: German
Currently translated at 90.4% (1804 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2025-12-16 11:00:25 +01:00
Alejandro Alonso 15ed25ca79 Merge pull request #7966 from penpot/niwinz-staging-abrreviate
🐛 Fix incorrect string truncation with abbreviate template filter
2025-12-12 13:53:33 +01:00
Andrey Antukh 9aa387a473 🐛 Fix incorrect string truncation with abbreviate template filter 2025-12-12 13:50:46 +01:00
Alejandro Alonso 67ba91b4b9 Merge pull request #7971 from penpot/niwinz-staging-bugfix-6
🐛 Fix tokens-lib encoding when value is nilable
2025-12-12 13:46:06 +01:00
Alejandro Alonso f67f1a6a0e Merge pull request #7972 from penpot/niwinz-staging-bugfix-7
🐛 Fix exception on assinging gradient to shadow on multiple selection
2025-12-12 13:42:39 +01:00
Alejandro Alonso 82d3e2024e Merge pull request #7973 from penpot/niwinz-staging-worker-scheduler
🐛 Fix incorrect redis connection error handling
2025-12-12 13:23:49 +01:00
Alejandro Alonso 4bd846c16d Merge pull request #7969 from penpot/niwinz-staging-fix-ratelimit
🐛 Fix issue on reading rlimit config
2025-12-12 13:22:53 +01:00
alonso.torres 8fde6b28ed 🐛 Fix problems with alignments and margins 2025-12-12 13:21:04 +01:00
alonso.torres 63325ec796 🐛 Fix problem with flex fill size distribution 2025-12-12 13:21:04 +01:00
alonso.torres 84415476d0 🐛 Fix problem with reflow layout 2025-12-12 13:21:04 +01:00
Andrey Antukh 94f95ca6b8 🐛 Fix incorrect redis connection error handling 2025-12-12 12:33:38 +01:00
Andrey Antukh 33c786498d Merge remote-tracking branch 'origin/staging-render' into develop 2025-12-12 12:19:49 +01:00
Andrey Antukh 5abc1aafb4 Merge tag '2.12.0-RC3' 2025-12-12 12:19:29 +01:00
Andrey Antukh 1f886b1f88 Merge remote-tracking branch 'origin/staging' into develop 2025-12-12 12:16:41 +01:00
Aitor Moreno 5a922c6bd6 Merge pull request #7960 from penpot/superalex-fix-too-many-active-webgl-contexts
🐛 Fix too many active WEBGL contexts
2025-12-12 12:03:46 +01:00
Andrey Antukh 507bf7445b 🐛 Fix tokens-lib encoding when value is nilable 2025-12-12 11:42:15 +01:00
Andrey Antukh 81b72c5acd 🐛 Fix exception on assinging gradient to shadow on multiple selection 2025-12-12 11:24:53 +01:00
Alejandro Alonso 1388865cfc 🐛 Fix too many active WEBGL contexts 2025-12-12 11:16:47 +01:00
Andrey Antukh 1738847694 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-12 10:20:21 +01:00
Aitor Moreno ca1c3c799d Merge pull request #7968 from penpot/alotor-fix-border-radius
🐛 Fix problem with border radius to path
2025-12-12 10:18:07 +01:00
Andrey Antukh 974495e08f Reduce log level for profile picture download error
Because it is not blocking operation and does not provents user
to proceed.
2025-12-12 08:17:13 +01:00
Andrey Antukh 2ed39e43c3 🐛 Fix issue on reading rlimit config 2025-12-11 23:50:01 +01:00
alonso.torres ce5006ae84 🐛 Fix problem with border radius to path 2025-12-11 22:40:44 +01:00
Eva Marco 50dbe6ab12 🐛 Fix horizontal scroll on layer panel (#7956) 2025-12-11 21:34:18 +01:00
Belén Albeza 0a7a65af5d ♻️ Make SerializableResult to depend on From traits 2025-12-11 16:00:03 +01:00
alonso.torres ea4d0e1238 Calculate position data in wasm 2025-12-11 16:00:03 +01:00
Elena Torro b705cf953a 🐛 Set layout data from set-object 2025-12-11 14:52:32 +01:00
Alejandro Alonso 90ce1f56e7 Merge pull request #7958 from penpot/superalex-fix-svg-extract-ids
🐛 Fix svg extract ids
2025-12-11 14:02:05 +01:00
Alejandro Alonso ab0438cc6f 🐛 Fix svg extract ids 2025-12-11 13:47:00 +01:00
Aitor Moreno c6aa9cc4b7 Merge pull request #7950 from penpot/ladybenko-12851-fix-text-selection
🐛 Fix text selection when editor regains focus
2025-12-11 13:45:29 +01:00
Andrey Antukh 5779adef33 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-11 13:30:59 +01:00
Andrey Antukh 2f46cbc0d4 Make render wasm import on worker http cache aware 2025-12-11 13:27:20 +01:00
Elena Torró ebf1758958 Merge pull request #7935 from penpot/superalex-improve-svg-import
🎉 Improve svg import
2025-12-11 13:21:29 +01:00
Elena Torró e94c56bfa7 Merge pull request #7954 from penpot/azazeln28-fix-font-weight-mixed-value
🐛 Fix font weight mixed value
2025-12-11 12:43:53 +01:00
Andrey Antukh 53be6f996b 🐛 Fix issues on build processs related to render-wasm 2025-12-11 12:41:19 +01:00
Alejandro Alonso 89d9591011 🎉 Improve svg import 2025-12-11 12:02:34 +01:00
Andrey Antukh 5a260294a1 🔧 Update build-tag.yml github workflow 2025-12-11 12:00:42 +01:00
Andrey Antukh 3becfcd723 🔧 Update build-tag.yml github workflow 2025-12-11 11:59:16 +01:00
Andrey Antukh 3f6e44316e 🐛 Add missing node depes install on render-wasm 2025-12-11 11:51:47 +01:00
Aitor Moreno 5501a2815f 🐛 Fix font-variant-id mixed value 2025-12-11 11:32:27 +01:00
Eva Marco 77ef8e6fe6 🐛 Fix scroll on move library modal (#7952) 2025-12-11 10:46:54 +01:00
Alejandro Alonso 1066438b02 Merge pull request #7922 from penpot/elenatorro-12855-improve-pan-rendering
🔧 Improve pan rendering
2025-12-10 15:58:59 +01:00
Alejandro Alonso 3b23a3ad19 Merge pull request #7947 from penpot/elenatorro-12880-fix-variant-ui
🔧 Support variants interactivity on the new render's UI
2025-12-10 15:27:48 +01:00
Andrey Antukh 7396f4bfb6 Merge remote-tracking branch 'origin/staging' into develop 2025-12-10 15:17:50 +01:00
Alejandro Alonso 916b7709dc Update Pencil Penpot Design System System template in carousel (#7948) 2025-12-10 15:09:28 +01:00
Belén Albeza 5cf51f3d26 🐛 Fix text selection not being restore if it was only 1 word 2025-12-10 15:05:13 +01:00
Belén Albeza 25acad5154 🔧 Add formatting rules to the TextEditor 2025-12-10 15:04:34 +01:00
Elena Torro 0a212b6291 🔧 Support variants interactivity on the new render's UI 2025-12-10 14:39:59 +01:00
Eva Marco 443e41fea4 🐛 Fix multiple selection with color tokens (#7941) 2025-12-10 14:36:08 +01:00
Alejandro Alonso c7c9b04095 Merge pull request #7944 from penpot/niwinz-staging-exporter-fix
🐛 Fix incorrect resource lifetime handling on exporter
2025-12-10 14:35:20 +01:00
Eva Marco c61a0c0332 📚 Add line to changelog (#7945) 2025-12-10 13:58:18 +01:00
Andrey Antukh eb1eeb4750 Merge remote-tracking branch 'origin/staging-render' into niwinz-develop-merge 2025-12-10 13:53:15 +01:00
Andrey Antukh a78477592b Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-10 13:36:23 +01:00
Eva Marco 8707ff6511 🎉 Add spanish translation 2025-12-10 13:12:30 +01:00
Florian Schroedl 3d8a251741 🐛 Disallow font-family referencing composite token 2025-12-10 13:12:30 +01:00
Andrey Antukh 34e84ee3c8 🐛 Fix incorrect resource lifetime handling on exporter 2025-12-10 13:02:31 +01:00
Xaviju 0956b66281 💄 Group tokens by name path (#7775)
* 💄 Group tokens by name path
2025-12-10 12:34:19 +01:00
Luis de Dios 007b3f11f9 🐛 Fix pass new icons to radio buttons (#7939) 2025-12-10 12:28:27 +01:00
Alejandro Alonso e8201402a7 Merge pull request #7938 from penpot/niwinz-staging-bugfix-5
🐛 Fix several issues
2025-12-10 12:05:42 +01:00
Aitor Moreno 8a22477b96 Merge pull request #7932 from penpot/niwinz-staging-worker-wasm-load
🐛 Fix WASM loading strategy on worker
2025-12-10 11:47:31 +01:00
Elena Torro a661b2564f 🐛 Fix default case on vertical align 2025-12-10 10:59:27 +01:00
Elena Torro 2c3732f3f4 🔧 Fix line height calculation 2025-12-10 10:59:27 +01:00
Andrey Antukh e16645227b Merge branch 'staging-render' into develop 2025-12-10 10:10:44 +01:00
Andrey Antukh 45665a3c21 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-10 10:09:58 +01:00
Alejandro Alonso 3e684ea54f ⬆️ Update svgo dependency on frontend (#7936) 2025-12-10 10:07:02 +01:00
Eva Marco 179e6a195d 🎉 Add test for token creation (#7915) 2025-12-10 09:56:21 +01:00
Andrey Antukh 98039f13d8 🐛 Fix main toolbar z-index 2025-12-10 09:47:40 +01:00
Alejandro Alonso 40c27591f6 🐛 Fix svg import (#7925) 2025-12-10 08:36:54 +01:00
Andrey Antukh 91d20a46d1 💄 Add cosmetic changes to exports assets progress component 2025-12-10 08:23:05 +01:00
Andrey Antukh 50bead7c56 🐛 Fix react warning on having p inside p on assets export progress 2025-12-10 08:22:41 +01:00
Andrey Antukh b75b999903 📎 Fix devenv jvm warning 2025-12-10 08:22:05 +01:00
Andrey Antukh 810f1721c8 🐛 Fix recursion render on subscription modal 2025-12-10 07:54:52 +01:00
Stephan Paternotte b45bdd723f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-12-09 22:00:21 +00:00
Ingrid Pigueron 8696044620 🌐 Add translations for: French
Currently translated at 97.1% (1937 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-09 22:00:19 +00:00
Andrey Antukh 8a8f360c7f Merge remote-tracking branch 'origin/staging' into develop 2025-12-09 19:53:38 +01:00
Andrey Antukh a4646373cf ♻️ Refactor wasm loading strategy on worker 2025-12-09 19:41:19 +01:00
Andrey Antukh f111cbb2a4 Add better cache config on devenv nginx 2025-12-09 19:38:30 +01:00
Luis de Dios e35fc85c3d 🎉 Create new empty-state component (#7903) 2025-12-09 16:48:12 +01:00
Aitor Moreno a614207f7e 🐛 Fix exporter failing with HTTPS 2025-12-09 16:08:20 +01:00
Elena Torro 81bc1bb0af 🔧 Log performance when building using profile-macros 2025-12-09 15:25:13 +01:00
Yamila Moreno 1798461d21 🐳 Add override for assets (#7926) 2025-12-09 14:55:21 +01:00
Luis de Dios 6ce3249c6d 🐛 Fix color format does not switch in the view mode (#7923)
* 🐛 Fix color format does not switch in the inspect mode of the view mode

* ♻️ Update components
2025-12-09 14:38:15 +01:00
Yamila Moreno dde0fddd6f 🐳 Add missing override to Dockerfile.frontend (#7920) 2025-12-09 12:08:46 +01:00
Aitor Moreno 7d36bc4025 Merge pull request #7907 from penpot/alotor-fix-export-text
🐛 Fix problem when exporting texts
2025-12-09 11:28:47 +01:00
Elena Torro b8feb6374d 🔧 Rebuild indices on zoom change, not pan 2025-12-09 11:26:03 +01:00
Elena Torro 0889df8e08 🔧 Skip slow operations on fast render 2025-12-09 11:26:03 +01:00
Andrey Antukh 4637aced8c Add support auto decoding and validation syntax for obj/reify 2025-12-09 11:13:06 +01:00
Andrey Antukh 9dfe5b0865 🐛 Fix inconsistencies on using obj/reify on plugins 2025-12-09 11:13:06 +01:00
Andrey Antukh 33bcc9544a Update frontend repl script 2025-12-09 11:13:06 +01:00
Andrey Antukh babd481b7f Make sm/coercer lazy 2025-12-09 11:13:06 +01:00
Andrey Antukh a9733c792d Make check-fn completly lazy 2025-12-09 11:13:06 +01:00
Belén Albeza 7be8ac3fd7 🐛 Fix internal error while importing a library 2025-12-09 11:10:32 +01:00
Pablo Alba b0351be724 🐛 Fix switch variants with paths 2025-12-09 11:08:55 +01:00
Elena Torro 9216d965ef 🔧 Update rendering settings to smooth render 2025-12-09 10:43:33 +01:00
Andrey Antukh d04fdb5fbd Make the dist bundle use consistent and cache-aware uris (#7911) 2025-12-09 08:05:28 +01:00
Ingrid Pigueron 4f3ca6422c 🌐 Add translations for: French
Currently translated at 96.8% (1931 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2025-12-08 19:40:14 +01:00
Nicola Bortoletto 1c03457fda 🌐 Add translations for: Italian
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-12-06 07:00:19 +00:00
Eva Marco 81e0e4f222 ♻️ Replace token form files (#7896)
* ♻️ Replace shadow form

* ♻️ Rename files and components

* ♻️ Replace offsetx and offsety names

* ♻️ Replace form file for new form component using new form system

* ♻️ Rename files and props
2025-12-05 17:04:07 +01:00
Andrey Antukh b8392b3731 🐛 Fix regression on sending team invitations (#7912) 2025-12-05 12:36:06 +01:00
Andrey Antukh 935728aa39 🔧 Backport build-tag github workflow from develop 2025-12-05 10:26:01 +01:00
Andrey Antukh 77dba477ca 🔧 Backport build-tag github workflow from develop 2025-12-05 10:25:03 +01:00
Eva Marco b6598d1f07 🐛 Fix scrollbar on color modal (#7906) 2025-12-05 09:55:41 +01:00
Yamila Moreno f13b3c8737 🔧 Fix bug in Github Actions (#7908) 2025-12-04 20:24:33 +01:00
alonso.torres 520e979363 🐛 Fix problem when exporting texts 2025-12-04 17:32:54 +01:00
Yamila Moreno a0f8559ffc 🔧 Add ci/cd for nitrate-module (#7905) 2025-12-04 16:02:29 +01:00
Xaviju bf1dc21c75 💄 Hide themes & sets panels when none active (#7902) 2025-12-04 14:11:57 +01:00
Alejandro Alonso 46c20a993f Merge pull request #7904 from penpot/niwinz-staging-fix-invitation-resend
🐛 Fix exception on resending invitation
2025-12-04 11:56:07 +01:00
Andrey Antukh 0e0106f69a 🐛 Add correct assertion on create-invitation fn 2025-12-04 11:38:32 +01:00
Andrey Antukh 19bb69cc60 Improve invalid schema error report 2025-12-04 11:38:16 +01:00
Alejandro Alonso 504eb70988 Merge pull request #7885 from penpot/niwinz-staging-bugfix-2
🐛 Make workspace palette reposition on left sidebar collapse
2025-12-04 11:19:20 +01:00
Andrey Antukh a38f425dd3 Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-04 11:06:48 +01:00
Xaviju 75a2331edf 💄 Set low-emphasis color for both light/dark modes (#7884) 2025-12-04 11:04:07 +01:00
VKing9 74d4b9b045 🌐 Add translations for: Hindi
Currently translated at 100.0% (1994 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2025-12-04 11:00:32 +01:00
Alejandro Alonso c2b4c9907d Merge pull request #7886 from penpot/niwinz-staging-bugfix-3
🐛 Fix casing on a translation of export files modal option
2025-12-04 10:59:51 +01:00
Alejandro Alonso bd5bbcae26 Merge pull request #7894 from penpot/niwinz-staging-bugfix-4
🐛 Fix incorrect interaction betwen hower and scroll on assets sidebar
2025-12-04 10:58:54 +01:00
Andrey Antukh 84273508ad 🐛 Fix incorrect interaction betwen hower and scroll on assets sidebar 2025-12-04 10:56:29 +01:00
Andrey Antukh 9245ba6bc2 💄 Adapt component style for assets-local-library on sidebar assets 2025-12-04 10:55:57 +01:00
Andrey Antukh 4be046406d Pass direct args instead of a vector to toggle-values on sidebar assets 2025-12-04 10:55:57 +01:00
Alejandro Alonso 84c747cd31 Merge pull request #7883 from penpot/niwinz-staging-bugfix
🐛 Fix exception on paste text on comments input
2025-12-04 10:32:07 +01:00
Alejandro Alonso 0036a9a0cd Merge pull request #7865 from penpot/niwinz-staging-audit
 Add minor improvements to the audit module
2025-12-04 10:04:00 +01:00
Alejandro Alonso 2105c3a68c Merge pull request #7866 from penpot/niwinz-staging-fix-emails
🐛 Change internal ordering on how email parts are assembled
2025-12-04 09:56:22 +01:00
Belén Albeza 38efa88460 🐛 Fix unpublish library modal not scrolling file list (#7892)
* 🐛 Fix unpublish library modal not scrolling when the linked files list is too long

* 💄 Remove deprecated tokens in unpublish library modal

* 🔧 Update CHANGELOG
2025-12-03 22:41:20 +01:00
Pablo Alba 6e254c2cf4 🐛 Fix change of library on swap (#7898) 2025-12-03 22:40:23 +01:00
Andrey Antukh 416980f063 🐛 Fix issue on render template on dist bundle (#7899) 2025-12-03 20:48:02 +01:00
Andrey Antukh f76710296c Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 18:52:28 +01:00
Andrey Antukh 6251fa6b22 🐛 Close other open context menus on open a context menu (#7895) 2025-12-03 18:50:00 +01:00
alonso.torres aedd8cc11e 🐛 Fix problem when renaming variants in plugins 2025-12-03 17:42:17 +01:00
Andrey Antukh d1379c55f6 Make i18n translation files load on demand 2025-12-03 16:44:37 +01:00
Andrey Antukh b125c7b5a3 Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 13:55:01 +01:00
Andrey Antukh 496d37795b Adapt docker images nginx config template to latest changes (#7891) 2025-12-03 13:45:18 +01:00
Alonso Torres 2f0853f5cc 🐛 Fix problem with variant plugins api (#7890) 2025-12-03 13:27:32 +01:00
Juan de la CruzandAndrey Antukh 648e660bcf 🎉 Add new content and images for the slides of 2.12 (#7874)
* 🎉 Add new slide's content

* 🎉 Add new slides images

* 📎 Fix clj fmt

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2025-12-03 13:26:55 +01:00
Andrey Antukh 9f6899007a Merge remote-tracking branch 'origin/staging' into develop 2025-12-03 13:10:30 +01:00
Slava "nerfur" Voronzoff bee2f70bfa 🐛 Add missing amd64 option on arch detection code on mange.sh script
Signed-off-by: Slava "nerfur" Voronzoff <nerfur@gmail.com>
2025-12-03 13:09:49 +01:00
Pablo Alba 00f8eac8fa 🐛 Fix can't delete unsaved variant prop (#7878) 2025-12-03 13:03:17 +01:00
Dalai FelintoandDalai Felinto df7caacb45 🐛 Fix crash in token grid view due to tooltip validation (#7887)
The color tokens in grid view have a tooltip which is a map.
This is done so the frontend can render:

```
Name: foo
Resolved value: #000000
```

However the validation scheme for tooltips was only accepting functions
and strings.

---

How to reproduce the original (unreported) crash:
* Create a color token
* Create a shape, add a fill
* Pick a color, chose the Token options
* Click on the Grid View

Crash: `{:hint "invalid props on component tooltip*\n\n  -> 'content'
    should be a string\n"}`

Signed-off-by: Dalai Felinto <dalai@blender.org>
Co-authored-by: Dalai Felinto <dalai@blender.org>
2025-12-03 13:01:36 +01:00
Marina López 641df77834 🐛 Fix wrong board size presets in Android (#7888) 2025-12-03 12:52:47 +01:00
Marina López 49bbdfb257 🐛 Fix U and E icon displayed in project list (#7875)
* 🐛 Fix U and E icon displayed in project lis

* 🐛 Fix U and E icon displayed in project list
2025-12-03 12:50:51 +01:00
Aitor Moreno 4e84deca44 Merge pull request #7879 from penpot/elenatorro-12797-fix-update-spans
🐛 Fix paragraph with text spans with multiple styles
2025-12-03 11:30:17 +01:00
Aitor Moreno 0d21e52068 🐛 Fix applyStylesTo entire selection 2025-12-03 11:07:33 +01:00
alonso.torres 1b29e9a50f 🐛 Fix race condition with fix fonts patch 2025-12-03 10:39:05 +01:00
Andrey Antukh 94af978be8 🐛 Fix casing on a translation of export files modal option 2025-12-03 10:22:45 +01:00
Andrey Antukh feababe2a8 🐛 Make workspace palette reposition on left sidebar collapse 2025-12-03 09:56:14 +01:00
Andrey Antukh 5ef06685fc 💄 Add cosmetic improvements to workspace palette component 2025-12-03 09:38:23 +01:00
Elena Torro 9f567c3bf4 🐛 Fix italic variant 2025-12-03 08:59:25 +01:00
Elena Torro 1ba15e5d10 🐛 Do not merge fill styles 2025-12-03 08:55:11 +01:00
Andrey Antukh 57fcec5afc 🐛 Make from-synthetic-clipboard-event function return always a stream
Causes an execption on steam processing when it returns nil
2025-12-03 08:32:38 +01:00
Andrey Antukh 58f82da61e 🐛 Fix exception on paste text on comments input 2025-12-03 08:20:58 +01:00
Andrey Antukh a28c5b61ca 💄 Adapt viewport paste code codestyle
And remove some not necessary constructions
2025-12-03 08:09:13 +01:00
Yaron Shahrabani 60df56caa3 🌐 Add translations for: Hebrew
Currently translated at 99.6% (1988 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2025-12-02 21:00:32 +00:00
Andrey Antukh 53aad7bc15 Merge remote-tracking branch 'origin/staging' into develop 2025-12-02 17:43:34 +01:00
Andrey Antukh 9123d199b7 🐛 Fix scripts/fmt 2025-12-02 17:43:21 +01:00
alonso.torres 37e45a8bbf 🐛 Fix race condition with text and type 2025-12-02 17:28:20 +01:00
alonso.torres 3471d40f46 🐛 Fix problem with boolean shapes updates 2025-12-02 17:28:20 +01:00
Elena Torro c6b64a8e39 🐛 Fix selectAll on mixed span styles 2025-12-02 16:50:48 +01:00
Elena Torro 511e80c948 🐛 Fix merge fill styles when there are multiple fills 2025-12-02 16:50:04 +01:00
Elena Torró f5a640d104 Merge pull request #7876 from penpot/ladybenko-12805-slow-loading
🐛 Fix viewport not being fully drawn on first load until a mouse …
2025-12-02 15:31:43 +01:00
Belén Albeza 3ae7c514e4 🐛 Fix viewport not being fully drawn on first load until a mouse hover 2025-12-02 15:06:28 +01:00
Andrey Antukh 57297741f5 Merge remote-tracking branch 'origin/staging' into develop 2025-12-02 13:28:50 +01:00
Andrey Antukh eeaf28bb25 📎 Disable caddy logging 2025-12-02 13:27:09 +01:00
Dalai Felinto d63d692d34 🐛 Fix mask issues with component swap #7675
The logic to swap a component would delete the swapped out component
first before bringing in the new one.

In the process of doing so, the sanitization code would unmask the
group, now orphan of its mask shape component, when it was the first
element of the group.

The fix  was to pass an optional argument to the generate-delete-shapes
function to ignore mask in special cases like this.

Signed-off-by: Dalai Felinto <dalai@blender.org>
2025-12-02 12:31:44 +01:00
alonso.torres fad9ed1c48 🐛 Fix problem with reordering layers 2025-12-02 12:27:00 +01:00
alonso.torres 0caaefefea 🐛 Fix outline with single click text creation 2025-12-02 11:08:58 +01:00
Elena Torro b179aa79b1 🐛 Fix create empty text on click regression 2025-12-02 11:08:58 +01:00
Andrey Antukh 6b8091bb90 Make devenv https and http2 capable (#7871)
Making it more similar on how it runs on production
environments and improves large amount of files loading
thanks to http2.
2025-12-02 10:49:37 +01:00
Andrey Antukh fe72d0af82 Add self-signed cert to caddy (#7872) 2025-12-02 10:45:26 +01:00
Aitor Moreno 405ddb60d8 🐛 Fix letter spacing applied to paragraph 2025-12-02 10:45:19 +01:00
Madalena MeloandAndres Gonzalez bba02473d5 📚 Update subtitles in the new user guide cards (#7823)
Co-authored-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
2025-12-02 09:21:05 +01:00
Elena Torró 95c0d42d5b Merge pull request #7868 from penpot/alotor-fix-flex-tools
🐛 Fix visual feedback on padding/margin/gaps modified
2025-12-01 17:51:44 +01:00
alonso.torres 721b337511 🐛 Fix visual feedback on padding/margin/gaps modified 2025-12-01 16:31:15 +01:00
Elena Torró 359379be09 Merge pull request #7867 from penpot/azazeln28-add-text-editor-v2-tests-to-staging
 Add text editor v2 integration tests
2025-12-01 16:11:25 +01:00
Aitor Moreno 876d5783cf Add text editor v2 integration tests 2025-12-01 15:56:52 +01:00
Elena Torro 786f73767b 🔧 Normalize font attributes to support old formats 2025-12-01 14:59:24 +01:00
Andrey Antukh 95b7784a42 🐛 Change internal ordering on how email parts are assembled
This fixes the html email rendering on gmail. Other clients (like proton,
emailcatcher) properly renders html independently of the order of parts
on the multipart email structure but gmail requires that html should be
the last one.
2025-12-01 14:27:21 +01:00
Andrey Antukh 4690f740b9 Add minor improvements to the audit module 2025-12-01 13:57:55 +01:00
Andrey Antukh 4282cdcd2c Merge remote-tracking branch 'origin/staging' into staging-render 2025-12-01 10:11:06 +01:00
Alejandro Alonso e889413f26 🐛 Fix nested shadows clipping 2025-12-01 09:22:23 +01:00
Elena Torró 115273b478 Merge pull request #7852 from penpot/alotor-flex-issues
🐛 Fix flex problems in new render
2025-11-28 14:10:42 +01:00
Elena Torró fdddd3284a Merge pull request #7859 from penpot/ladybenko-12801-fix-mismatched-fonts
🐛 Fix mismatch between fonts for rendered and selected text when no fallback fonts apply
2025-11-28 14:10:17 +01:00
Belén Albeza 51385a04a0 🐛 Fix mismatch between fonts for rendered and selected text when no fallback fonts apply 2025-11-28 13:54:17 +01:00
Belén Albeza f96ed8ccd6 Fix playwright tests 2025-11-28 13:25:13 +01:00
Belén Albeza bda5de5c1b 🔧 Update google fonts list 2025-11-28 13:25:13 +01:00
alonso.torres 59f3b4db4c 🐛 Fix problem with auto-size and element margins 2025-11-28 12:12:19 +01:00
alonso.torres 7ee03ad911 🐛 Fix problem with grid layout editor 2025-11-28 12:12:09 +01:00
alonso.torres 130b8c8214 🐛 Fix problems with flex layout in new render 2025-11-28 10:49:55 +01:00
alonso.torres 0198d41757 🐛 Fix crash when cleanup 2025-11-28 10:44:54 +01:00
alonso.torres 567a955151 🐛 Fix problem with change gap/margin/padding 2025-11-28 10:44:38 +01:00
Nicola Bortoletto 34da754357 🌐 Add translations for: Italian
Currently translated at 98.9% (1973 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2025-11-28 05:00:28 +00:00
Oğuz Ersen 39eafae251 🌐 Add translations for: Turkish
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2025-11-25 16:51:25 +00:00
Edgars Andersons e1e09b7f96 🌐 Add translations for: Latvian
Currently translated at 94.0% (1876 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2025-11-25 16:51:24 +00:00
Stephan Paternotte 3b39980f2f 🌐 Add translations for: Dutch
Currently translated at 99.8% (1991 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2025-11-25 16:51:23 +00:00
Anton Palmqvist 223b12d2c7 🌐 Add translations for: Swedish
Currently translated at 99.6% (1987 of 1994 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2025-11-25 16:51:21 +00:00
1581 changed files with 217210 additions and 82803 deletions

No files matched your search

+9
View File
@@ -45,6 +45,15 @@
:potok/reify-type
{:level :error}
:redundant-primitive-coercion
{:level :off}
:unused-excluded-var
{:level :off}
:unresolved-excluded-var
{:level :off}
:missing-protocol-method
{:level :off}
+5
View File
@@ -2,6 +2,11 @@
:remove-multiple-non-indenting-spaces? false
:remove-surrounding-whitespace? true
:remove-consecutive-blank-lines? false
:indent-line-comments? true
:parallel? true
:align-form-columns? false
;; :align-map-columns? false
;; :align-single-column-lines? false
:extra-indents {rumext.v2/fnc [[:inner 0]]
cljs.test/async [[:inner 0]]
promesa.exec/thread [[:inner 0]]
@@ -0,0 +1,38 @@
---
name: New Render Bug Report
about: Create a report about the bugs you have found in the new render
title: ''
labels: new render
assignees: claragvinola
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or screen recordings**
If applicable, add screenshots or screen recording to help illustrate your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+2 -2
View File
@@ -40,7 +40,7 @@ on:
jobs:
build-bundle:
name: Build and Upload Penpot Bundle
runs-on: ubuntu-24.04
runs-on: penpot-runner-01
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -48,7 +48,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
+10 -5
View File
@@ -7,23 +7,28 @@ jobs:
build-and-push:
name: Build and push DevEnv Docker image
environment: release-admins
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Build and push DevEnv Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'penpotapp/devenv'
with:
+40 -9
View File
@@ -19,11 +19,16 @@ on:
jobs:
build-and-push:
name: Build and Push Penpot Docker Images
runs-on: ubuntu-24.04-arm
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
@@ -54,32 +59,43 @@ jobs:
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
mv penpot/mcp bundle-mcp
popd
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the “429 Too Many Requests” error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
uses: docker/login-action@v4
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images:
frontend
backend
exporter
storybook
mcp
labels: |
bundle_version=${{ steps.bundles.outputs.bundle_version }}
- name: Build and push Backend Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'backend'
BUNDLE_PATH: './bundle-backend'
@@ -94,7 +110,7 @@ jobs:
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Frontend Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'frontend'
BUNDLE_PATH: './bundle-frontend'
@@ -109,7 +125,7 @@ jobs:
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Exporter Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'exporter'
BUNDLE_PATH: './bundle-exporter'
@@ -124,7 +140,7 @@ jobs:
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push Storybook Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'storybook'
BUNDLE_PATH: './bundle-storybook'
@@ -138,6 +154,21 @@ jobs:
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push MCP Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'mcp'
BUNDLE_PATH: './bundle-mcp'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
+22
View File
@@ -0,0 +1,22 @@
name: _MAIN-STAGING
on:
workflow_dispatch:
schedule:
- cron: '26 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "main-staging"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
uses: ./.github/workflows/build-docker.yml
secrets: inherit
with:
gh_ref: "main-staging"
@@ -0,0 +1,14 @@
name: _STAGING RENDER
on:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"
+3 -2
View File
@@ -11,7 +11,7 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "no"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
@@ -23,6 +23,7 @@ jobs:
notify:
name: Notifications
runs-on: ubuntu-24.04
needs: build-docker
steps:
@@ -32,7 +33,7 @@ jobs:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🐳 *[PENPOT] Docker image available.*
🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
+4 -1
View File
@@ -6,12 +6,14 @@ on:
- edited
- reopened
- synchronize
- ready_for_review
pull_request_target:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
push:
branches:
- main
@@ -20,13 +22,14 @@ on:
jobs:
check-commit-message:
if: ${{ !github.event.pull_request.draft }}
name: Check Commit Message
runs-on: ubuntu-latest
steps:
- name: Check Commit Type
uses: gsactions/commit-message-checker@v2
with:
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$'
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert|Reapply).+[^.])$'
flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
@@ -0,0 +1,142 @@
name: Plugins/api-doc deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/libs/plugin-types/index.d.ts'
- 'plugins/libs/plugin-types/REAME.md'
- 'plugins/tools/typedoc.css'
- 'plugins/CHANGELOG.md'
- 'plugins/wrangler-penpot-plugins-api-doc.toml'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: Build docs
working-directory: plugins
shell: bash
run: pnpm run build:doc
- name: Select Worker name
run: |
REF="${{ steps.vars.outputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml
- name: Add noindex header and robots.txt files for non-production environments
if: ${{ steps.vars.outputs.gh_ref != 'main' }}
working-directory: plugins
shell: bash
run: |
ASSETS_DIR="dist/doc"
cat > "${ASSETS_DIR}/_headers" << 'EOF'
/*
X-Robots-Tag: noindex, nofollow
EOF
cat > "${ASSETS_DIR}/robots.txt" << 'EOF'
User-agent: *
Disallow: /
EOF
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config wrangler-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩📚 *[PENPOT PLUGINS] Error deploying API documentation.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
@@ -0,0 +1,127 @@
name: Plugins/package deployer
on:
# Deploy package from manual action
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
plugin_name:
description: 'Pluging name (like plugins/apps/<plugin_name>-plugin)'
type: string
required: true
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch'
type: string
required: true
default: 'develop'
plugin_name:
description: 'Publig name (from plugins/apps/<plugin_name>-plugin)'
type: string
required: true
permissions:
contents: read
jobs:
deploy:
runs-on: penpot-runner-01
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
working-directory: plugins
shell: bash
run: pnpm --filter ${{ inputs.plugin_name }}-plugin build
- name: Select Worker name
run: |
REF="${{ inputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pro" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pre" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-hourly" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" apps/${{ inputs.plugin_name }}-plugin/wrangler.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config apps/${{ inputs.plugin_name }}-plugin/wrangler.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩📦 *[PENPOT PLUGINS] Error deploying ${{ env.WORKER_NAME }}.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
Plugin name: `${{ inputs.plugin_name }}-plugin`
Cloudflare worker name: `${{ env.WORKER_NAME }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
@@ -0,0 +1,143 @@
name: Plugins/packages deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/apps/*-plugin/**'
- 'libs/plugins-styles/**'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
colors_to_tokens: ${{ steps.filter.outputs.colors_to_tokens }}
create_palette: ${{ steps.filter.outputs.create_palette }}
lorem_ipsum: ${{ steps.filter.outputs.lorem_ipsum }}
rename_layers: ${{ steps.filter.outputs.rename_layers }}
contrast: ${{ steps.filter.outputs.contrast }}
icons: ${{ steps.filter.outputs.icons }}
poc_state: ${{ steps.filter.outputs.poc_state }}
table: ${{ steps.filter.outputs.table }}
# [For new plugins]
# Add more outputs here
steps:
- uses: actions/checkout@v6
- id: filter
uses: dorny/paths-filter@v4
with:
filters: |
colors_to_tokens:
- 'plugins/apps/colors-to-tokens-plugin/**'
- 'libs/plugins-styles/**'
contrast:
- 'plugins/apps/contrast-plugin/**'
- 'libs/plugins-styles/**'
create_palette:
- 'plugins/apps/create-palette-plugin/**'
- 'libs/plugins-styles/**'
icons:
- 'plugins/apps/icons-plugin/**'
- 'libs/plugins-styles/**'
lorem_ipsum:
- 'plugins/apps/lorem-ipsum-plugin/**'
- 'libs/plugins-styles/**'
rename_layers:
- 'plugins/apps/rename-layers-plugin/**'
- 'libs/plugins-styles/**'
table:
- 'plugins/apps/table-plugin/**'
- 'libs/plugins-styles/**'
# [For new plugins]
# Add more plugin filters here
# another_plugin:
# - 'plugins/apps/another-plugin/**'
# - 'libs/plugins-styles/**'
colors-to-tokens-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.colors_to_tokens == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: colors-to-tokens
contrast-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.contrast == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: contrast
create-palette-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.create_palette == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: create-palette
icons-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.icons == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: icons
lorem-ipsum-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.lorem_ipsum == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: lorem-ipsum
rename-layers-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.rename_layers == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: rename-layers
table-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.table == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: table
# [For new plugins]
# Add more jobs for other plugins below, following the same pattern
# another-plugin:
# needs: detect-changes
# if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.another_plugin == 'true'
# uses: ./.github/workflows/plugins-deploy-package.yml
# secrets: inherit
# with:
# gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
# plugin_name: another
@@ -0,0 +1,140 @@
name: Plugins/styles-doc deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/apps/example-styles/**'
- 'plugins/libs/plugins-styles/**'
- 'plugins/wrangler-penpot-plugins-styles-doc.toml'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: Build styles
working-directory: plugins
shell: bash
run: pnpm run build:styles-example
- name: Select Worker name
run: |
REF="${{ steps.vars.outputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=penpot-plugins-styles-doc-pro" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=penpot-plugins-styles-doc-pre" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=penpot-plugins-styles-doc-hourly" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-styles-doc.toml
- name: Add noindex header and robots.txt files for non-production environments
if: ${{ steps.vars.outputs.gh_ref != 'main' }}
working-directory: plugins
shell: bash
run: |
ASSETS_DIR="dist/apps/example-styles"
cat > "${ASSETS_DIR}/_headers" << 'EOF'
/*
X-Robots-Tag: noindex, nofollow
EOF
cat > "${ASSETS_DIR}/robots.txt" << 'EOF'
User-agent: *
Disallow: /
EOF
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config wrangler-penpot-plugins-styles-doc.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩💅 *[PENPOT PLUGINS] Error deploying Styles documentation.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
+5 -4
View File
@@ -31,7 +31,7 @@ jobs:
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
@@ -63,14 +63,15 @@ jobs:
echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io
IMAGES=("frontend" "backend" "exporter" "storybook")
IMAGES=("frontend" "backend" "exporter" "mcp" "storybook")
SHORT_TAG=${TAG%.*}
for image in "${IMAGES[@]}"; do
skopeo copy --all \
docker://$DOCKER_REGISTRY/$image:$TAG \
docker://docker.io/penpotapp/$image:$TAG
for alias in main latest; do
for alias in main latest "$SHORT_TAG"; do
skopeo copy --all \
docker://$DOCKER_REGISTRY/$image:$TAG \
docker://docker.io/penpotapp/$image:$alias
@@ -93,7 +94,7 @@ jobs:
# --- Create GitHub release ---
- name: Create GitHub release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+47
View File
@@ -0,0 +1,47 @@
name: "MCP CI"
on:
pull_request:
branches:
- develop
- staging
- main
types:
- opened
- synchronize
- ready_for_review
paths:
- 'mcp/**'
push:
branches:
- develop
- staging
- main
paths:
- 'mcp/**'
jobs:
test-mcp:
if: ${{ !github.event.pull_request.draft }}
name: "Test MCP"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup
working-directory: ./mcp
run: ./scripts/setup
- name: Check
working-directory: ./mcp
run: |
pnpm run fmt:check;
pnpm -r run build;
pnpm -r run types:check;
+153 -79
View File
@@ -9,6 +9,7 @@ on:
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
@@ -20,45 +21,140 @@ concurrency:
jobs:
lint:
if: ${{ !github.event.pull_request.draft }}
name: "Linter"
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Check clojure code format
run: |
./scripts/lint
test-common:
name: "Common Tests"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run tests on JVM
- name: Lint Common
working-directory: ./common
run: |
clojure -M:dev:test
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run check-fmt:js
pnpm run lint:clj
- name: Run tests on NODE
- name: Lint Frontend
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:js
pnpm run check-fmt:clj
pnpm run check-fmt:scss
pnpm run lint:clj
pnpm run lint:js
pnpm run lint:scss
- name: Lint Backend
working-directory: ./backend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
- name: Lint Exporter
working-directory: ./exporter
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
- name: Lint Library
working-directory: ./library
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
test-common:
if: ${{ !github.event.pull_request.draft }}
name: "Common Tests"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run tests
working-directory: ./common
run: |
./scripts/test
test-plugins:
if: ${{ !github.event.pull_request.draft }}
name: Plugins Runtime Linter & Tests
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- uses: actions/checkout@v6
- name: Setup Node
id: setup-node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install;
- name: Run Lint
working-directory: ./plugins
run: pnpm run lint
- name: Run Format Check
working-directory: ./plugins
run: pnpm run format:check
- name: Run Test
working-directory: ./plugins
run: pnpm run test
- name: Build runtime
working-directory: ./plugins
run: pnpm run build:runtime
- name: Build doc
working-directory: ./plugins
run: pnpm run build:doc
- name: Build plugins
working-directory: ./plugins
run: pnpm run build:plugins
- name: Build styles
working-directory: ./plugins
run: pnpm run build:styles-example
test-frontend:
if: ${{ !github.event.pull_request.draft }}
name: "Frontend Tests"
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Unit Tests
working-directory: ./frontend
@@ -67,17 +163,20 @@ jobs:
- name: Component Tests
working-directory: ./frontend
env:
VITEST_BROWSER_TIMEOUT: 120000
run: |
./scripts/test-components
test-render-wasm:
if: ${{ !github.event.pull_request.draft }}
name: "Render WASM Tests"
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Format
working-directory: ./render-wasm
@@ -95,8 +194,9 @@ jobs:
./test
test-backend:
if: ${{ !github.event.pull_request.draft }}
name: "Backend Tests"
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
services:
@@ -120,7 +220,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Run tests
working-directory: ./backend
@@ -134,13 +234,14 @@ jobs:
clojure -M:dev:test --reporter kaocha.report/documentation
test-library:
if: ${{ !github.event.pull_request.draft }}
name: "Library Tests"
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Run tests
working-directory: ./library
@@ -148,38 +249,39 @@ jobs:
./scripts/test
build-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle"
runs-on: ubuntu-24.04
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build Bundle
working-directory: ./frontend
run: |
./scripts/build 0.0.0
./scripts/build
- name: Store Bundle Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: ubuntu-24.04
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests 1/3"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
@@ -187,10 +289,10 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="1/4";
./scripts/test-e2e --shard="1/3";
- name: Upload test result
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result-1
@@ -199,17 +301,18 @@ jobs:
retention-days: 3
test-integration-2:
name: "Integration Tests 2/4"
runs-on: ubuntu-24.04
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests 2/3"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
@@ -217,10 +320,10 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="2/4";
./scripts/test-e2e --shard="2/3";
- name: Upload test result
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result-2
@@ -229,17 +332,18 @@ jobs:
retention-days: 3
test-integration-3:
name: "Integration Tests 3/4"
runs-on: ubuntu-24.04
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests 3/3"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore Cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
@@ -247,43 +351,13 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="3/4";
./scripts/test-e2e --shard="3/3";
- name: Upload test result
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-tests-result-3
path: frontend/test-results/
overwrite: true
retention-days: 3
test-integration-4:
name: "Integration Tests 4/4"
runs-on: ubuntu-24.04
container: penpotapp/devenv:latest
needs: build-integration
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Restore Cache
uses: actions/cache/restore@v4
with:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
- name: Run Tests
working-directory: ./frontend
run: |
./scripts/test-e2e --shard="4/4";
- name: Upload test result
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-tests-result-4
path: frontend/test-results/
overwrite: true
retention-days: 3
+16 -13
View File
@@ -1,10 +1,4 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
*-init.clj
*.css.json
*.jar
@@ -19,7 +13,6 @@
.nyc_output
.rebel_readline_history
.repl
.shadow-cljs
/*.jpg
/*.md
/*.png
@@ -31,8 +24,13 @@
/.clj-kondo/.cache
/_dump
/notes
/.opencode/package-lock.json
/plans
/prompts
/playground/
/backend/*.md
!/backend/AGENTS.md
/backend/.shadow-cljs
/backend/*.sql
/backend/*.txt
/backend/assets/
@@ -43,27 +41,33 @@
/backend/resources/public/media
/backend/target/
/backend/experiments
/backend/scripts/_env.local
/bundle*
/cd.md
/clj-profiler/
/common/coverage
/common/target
/deploy
/common/.shadow-cljs
/docker/images/bundle*
/exporter/target
/exporter/.shadow-cljs
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
/frontend/text-editor/src/wasm/
/frontend/dist/
/frontend/npm-debug.log
/frontend/out/
/frontend/package-lock.json
/frontend/resources/fonts/experiments
/frontend/resources/public/*
/frontend/src/app/render_wasm/api/shared.js
/frontend/storybook-static/
/frontend/target/
/frontend/test-results/
/frontend/.shadow-cljs
/other/
/scripts/
/telemetry/
/nexus/
/tmp/
/vendor/**/target
/vendor/svgclean/bundle*.js
@@ -71,13 +75,12 @@
/library/target/
/library/*.zip
/external
clj-profiler/
node_modules
/penpot-nitrate
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/render-wasm/target/
/**/node_modules
/**/.yarn/*
/.pnpm-store
-105
View File
@@ -1,105 +0,0 @@
image:
file: docker/gitpod/Dockerfile
ports:
# nginx
- port: 3449
onOpen: open-preview
# frontend nREPL
- port: 3447
onOpen: ignore
visibility: private
# frontend shadow server
- port: 3448
onOpen: ignore
visibility: private
# backend
- port: 6060
onOpen: ignore
# exporter shadow server
- port: 9630
onOpen: ignore
visibility: private
# exporter http server
- port: 6061
onOpen: ignore
# mailhog web interface
- port: 8025
onOpen: ignore
# mailhog postfix
- port: 1025
onOpen: ignore
# postgres
- port: 5432
onOpen: ignore
# redis
- port: 6379
onOpen: ignore
# openldap
- port: 389
onOpen: ignore
tasks:
# https://github.com/gitpod-io/gitpod/issues/666#issuecomment-534347856
- name: gulp
command: >
cd $GITPOD_REPO_ROOT/frontend/;
yarn && gp sync-done 'frontend-yarn';
npx gulp --theme=${PENPOT_THEME} watch
- name: frontend shadow watch
command: >
cd $GITPOD_REPO_ROOT/frontend/;
gp sync-await 'frontend-yarn';
npx shadow-cljs watch main
- init: gp await-port 5432 && psql -f $GITPOD_REPO_ROOT/docker/gitpod/files/postgresql_init.sql
name: backend
command: >
cd $GITPOD_REPO_ROOT/backend/;
./scripts/start-dev
- name: exporter shadow watch
command:
cd $GITPOD_REPO_ROOT/exporter/;
gp sync-await 'frontend-yarn';
yarn && npx shadow-cljs watch main
- name: exporter web server
command: >
cd $GITPOD_REPO_ROOT/exporter/;
./scripts/wait-and-start.sh
- name: signed terminal
before: >
[[ ! -z ${GNUGPG} ]] &&
cd ~ &&
rm -rf .gnupg &&
echo ${GNUGPG} | base64 -d | tar --no-same-owner -xzvf -
init: >
[[ ! -z ${GNUGPG_KEY} ]] &&
git config --global commit.gpgsign true &&
git config --global user.signingkey ${GNUGPG_KEY}
command: cd $GITPOD_REPO_ROOT
- name: redis
command: redis-server
- before: go get github.com/mailhog/MailHog
name: mailhog
command: MailHog
- name: Nginx
command: >
nginx &&
multitail /var/log/nginx/access.log -I /var/log/nginx/error.log
+1 -1
View File
@@ -1 +1 @@
v22.19.0
v22.22.0
+33
View File
@@ -0,0 +1,33 @@
---
name: commiter
description: Git commit assistant following CONTRIBUTING.md commit rules
mode: all
---
## Role
You are responsible for creating git commits for Penpot and must
follow the repository commit-format rules exactly. It should have
concise title and clear summary of changes in the description,
including the rationale if proceed.
## Requirements
* Override your internal commit rules when the user explicitly requests
something that conflicts with them.
* Read `CONTRIBUTING.md` before creating any commit and follow the
commit guidelines strictly.
* Use commit messages in the form `:emoji: <imperative subject>`.
* Keep the subject capitalized, concise, 70 characters or fewer, and
without a trailing period.
* Keep the description (commit body) with maximum line length of 80
characters. Use manual line breaks to wrap text before it exceeds
this limit.
* Separate the subject from the body with a blank line.
* Write a clear and concise body when needed.
* Use `git commit -s` so the commit includes the required
`Signed-off-by` line.
* Do not guess or hallucinate git author information (Name or
Email). Never include the `--author` flag in git commands unless
specifically instructed by the user for a unique case; assume the
local environment is already configured.
+37
View File
@@ -0,0 +1,37 @@
---
name: Penpot Engineer
description: Senior Full-Stack Software Engineer
mode: primary
---
Role: You are a high-autonomy Senior Full-Stack Software Engineer working on
Penpot, an open-source design tool. You have full permission to navigate the
codebase, modify files, and execute commands to fulfill your tasks. Your goal is
to solve complex technical tasks with high precision while maintaining a strong
focus on maintainability and performance.
Tech stack: Clojure (backend), ClojureScript (frontend/exporter), Rust/WASM
(render-wasm), TypeScript (plugins/mcp), SCSS.
Requirements:
* Read the root `AGENTS.md` to understand the repository and application
architecture. Then read the `AGENTS.md` **only** for each affected module.
Not all modules have one — verify before reading.
* Before writing code, analyze the task in depth and describe your plan. If the
task is complex, break it down into atomic steps.
* When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
`.gitignore` by default.
* Do **not** touch unrelated modules unless the task explicitly requires it.
* Only reference functions, namespaces, or APIs that actually exist in the
codebase. Verify their existence before citing them. If unsure, search first.
* Be concise and autonomous — avoid unnecessary explanations.
* After making changes, run the applicable lint and format checks for the
affected module before considering the work done (see module `AGENTS.md` for
exact commands).
* Make small and logical commits following the commit guideline described in
`CONTRIBUTING.md`. Commit only when explicitly asked.
- Do not guess or hallucinate git author information (Name or Email). Never include the
`--author` flag in git commands unless specifically instructed by the user for a unique
case; assume the local environment is already configured. Allow git commit to
automatically pull the identity from the local git config `user.name` and `user.email`.
+64
View File
@@ -0,0 +1,64 @@
---
name: Penpot Planner
description: Software architect for planning and analysis only
mode: primary
permission:
edit: ask
---
# Penpot Planner
## Role
You are a Senior Software Architect working on Penpot, an open-source design
tool. Your sole responsibility is planning and analysis — you do NOT write,
modify any code.
You help users understand the codebase, design solutions, and create detailed
implementation plans that other agents or developers can execute. Document
everything they need to know: which files to touch for each task, code, testing,
docs they might need to check, how to test it. Give them the whole plan as
bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the developer's responsibility.
Assume they are a skilled developer, but know almost nothing about our toolset
or problem domain. Assume they don't know good test design very well.
## Requirements
* Analyze the codebase architecture and identify affected modules.
* Read `AGENTS.md` files (root and per-module) to understand structure and
conventions.
* Search code using `ripgrep` skill (`rg`) to trace dependencies, find patterns,
and understand existing implementations.
* Break down complex features or bugs into atomic, actionable steps.
* Propose solutions with clear rationale, trade-offs, and sequencing.
* Identify risks, edge cases, and testing considerations.
Save plans to: plans/YYYY-MM-DD-<plan-one-line-title>.md
## Constraints
* You are **read-only** — never create, edit, or delete files.
* You do **not** run builds, tests, linters, or any commands that modify state.
* You do **not** create git commits or interact with version control.
* You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`).
* Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output format
When producing a plan, structure it as:
1. **Context** — What is the problem or feature request?
2. **Affected modules** — Which parts of the codebase are involved?
3. **Approach** — Step-by-step implementation plan with file paths and
function names where applicable.
4. **Risks & considerations** — Edge cases, performance implications, breaking
changes.
5. **Testing strategy** — How to verify the implementation works correctly.
+59
View File
@@ -0,0 +1,59 @@
---
name: Prompt Assistant
description: Refines and improves prompts for maximum clarity and effectiveness
mode: all
---
# Prompt Assistant
## Role
You are an expert Prompt Engineer with strong knowledge of
penpot. Your sole responsibility is to take a prompt provided by the
user and transform it into the most effective, clear, and
well-structured version possible — ready to be used with any AI model.
## Requirements
* You do NOT execute tasks. You do NOT write code. You only design and
refine prompts
* Read the root `AGENTS.md` to understand the repository and application
architecture. Then read the `AGENTS.md` **only** for each affected module.
* Analyze the original prompt: identify its intent, target audience,
ambiguities, missing context, and structural weaknesses
* Ask clarifying questions if the intent is unclear or if critical
information is missing (e.g. target model, expected output format,
tone, constraints). Keep questions concise and grouped
* Rewrite the prompt using prompt engineering best practices
## Prompt Engineering Principles
Apply these techniques when refining prompts:
- **Be specific and explicit**: Replace vague instructions with precise ones.
- **Set the context**: Include background information the model needs to
perform well.
- **Specify the output format**: State the desired structure, length, tone,
or format (e.g. bullet list, JSON, step-by-step).
- **Add constraints**: Include what the model should avoid or not do.
- **Use examples** (few-shot): When applicable, suggest adding examples to
anchor the model's behaviour.
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
- **Avoid ambiguity**: Remove pronouns and references that could be
misinterpreted.
- **Chain of thought**: For reasoning tasks, include "Think step by step."
## Constraints
- Do NOT execute the prompt yourself.
- Do NOT answer the question inside the prompt.
- Do NOT add unnecessary verbosity — prompts should be as short as they can
be while remaining complete.
- Always preserve the user's original intent.
## Output
Refined Prompt: The improved, ready-to-use prompt. Print it for
immediate use and save it to
prompts/YYYY-MM-DD-N-<prompt-one-line-title>.md for future use.
+90
View File
@@ -0,0 +1,90 @@
---
name: backport-commit
description: Port changes from a specific Git commit to the current branch by manually applying the diff, avoiding cherry-pick when it would introduce complex conflicts.
---
# Backport Commit
Port changes from a specific Git commit to the current branch by manually
applying the diff, avoiding `git cherry-pick` when it would introduce
complex conflicts.
## When to Use
Use this skill whenever the user asks to backport a commit, especially when:
- The commit touches multiple modules or files with significant divergence
- `git cherry-pick` is explicitly ruled out ("do not use cherry-pick")
- The target commit is old enough that conflicts are likely
- The commit introduces both source changes AND new files (tests, etc.)
- You need full control over how each hunk is applied
## Workflow
### 1. Identify the target commit
```bash
# Verify the commit exists and understand what it does
git log --oneline -1 <commit-sha>
# Get the full diff (including new/deleted files)
git show <commit-sha>
# Capture the original commit message for later reuse
git log --format='%B' -1 <commit-sha>
```
### 2. Identify affected modules
From the file paths in the diff, determine which Penpot modules are affected
(frontend, backend, common, render-wasm, etc.) and read their `AGENTS.md`
files **before** making any changes. If a module has no `AGENTS.md`, skip
that step — verify with `ls <module>/AGENTS.md` first.
### 3. Read the current state of each affected file
For every file the diff touches, read the current version on disk to understand
context and ensure correct placement before editing.
### 4. Apply changes manually (the core of this approach)
Process every hunk in the diff using the appropriate tool:
| Diff action | Tool to use |
|-------------|-------------|
| Modify existing file | `edit` — use enough surrounding context in `oldString` to uniquely match the location |
| Add new file | `write` — include proper license header and namespace conventions matching project style |
| Delete file | `bash rm <path>` |
| Rename/move file | `bash mv <old> <new>`, then apply any content changes with `edit` |
> **Tip:** Group nearby hunks from the same file into a single `edit` call.
> Use separate calls when hunks are far apart to keep `oldString` short and
> unambiguous.
Repeat until **all** hunks in the diff are ported.
### 5. Validate
Run **lint**, **check-fmt**, and **tests** for every affected module (see each
module's `AGENTS.md` for the exact commands). If the formatter auto-fixes
indentation, verify the logic is still semantically correct. All checks must
pass before moving on.
### 6. Port the changelog entry (if any)
If the original commit added or modified a `CHANGES.md` entry, port that entry
too — adapting wording and version references for the target branch.
### 7. Commit
Ask the `commiter` sub-agent to create a commit. Stage all relevant files
(exclude unrelated untracked files) and provide the original commit message as
a reference, adapting it as needed for the target branch context.
## Key Principles
- **Context matters** — always read files before editing; never guess
indentation or surrounding code
- **Lint + format + test** — never skip validation before committing
- **Preserve intent** — keep the original commit message meaning; the
`commiter` agent handles formatting
+210
View File
@@ -0,0 +1,210 @@
---
name: bat-cat
description: A cat clone with syntax highlighting, line numbers, and Git integration - a modern replacement for cat.
homepage: https://github.com/sharkdp/bat
metadata: {"clawdbot":{"emoji":"🦇","requires":{"bins":["bat"]},"install":[{"id":"brew","kind":"brew","formula":"bat","bins":["bat"],"label":"Install bat (brew)"},{"id":"apt","kind":"apt","package":"bat","bins":["bat"],"label":"Install bat (apt)"}]}}
---
# bat - Better cat
`cat` with syntax highlighting, line numbers, and Git integration.
## Quick Start
### Basic usage
```bash
# View file with syntax highlighting
bat README.md
# Multiple files
bat file1.js file2.py
# With line numbers (default)
bat script.sh
# Without line numbers
bat -p script.sh
```
### Viewing modes
```bash
# Plain mode (like cat)
bat -p file.txt
# Show non-printable characters
bat -A file.txt
# Squeeze blank lines
bat -s file.txt
# Paging (auto for large files)
bat --paging=always file.txt
bat --paging=never file.txt
```
## Syntax Highlighting
### Language detection
```bash
# Auto-detect from extension
bat script.py
# Force specific language
bat -l javascript config.txt
# Show all languages
bat --list-languages
```
### Themes
```bash
# List available themes
bat --list-themes
# Use specific theme
bat --theme="Monokai Extended" file.py
# Set default theme in config
# ~/.config/bat/config: --theme="Dracula"
```
## Line Ranges
```bash
# Show specific lines
bat -r 10:20 file.txt
# From line to end
bat -r 100: file.txt
# Start to specific line
bat -r :50 file.txt
# Multiple ranges
bat -r 1:10 -r 50:60 file.txt
```
## Git Integration
```bash
# Show Git modifications (added/removed/modified lines)
bat --diff file.txt
# Show decorations (Git + file header)
bat --decorations=always file.txt
```
## Output Control
```bash
# Output raw (no styling)
bat --style=plain file.txt
# Customize style
bat --style=numbers,changes file.txt
# Available styles: auto, full, plain, changes, header, grid, numbers, snip
bat --style=header,grid,numbers file.txt
```
## Common Use Cases
**Quick file preview:**
```bash
bat file.json
```
**View logs with syntax highlighting:**
```bash
bat error.log
```
**Compare files visually:**
```bash
bat --diff file1.txt
bat file2.txt
```
**Preview before editing:**
```bash
bat config.yaml && vim config.yaml
```
**Cat replacement in pipes:**
```bash
bat -p file.txt | grep "pattern"
```
**View specific function:**
```bash
bat -r 45:67 script.py # If function is on lines 45-67
```
## Integration with other tools
**As pager for man pages:**
```bash
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
man grep
```
**With ripgrep:**
```bash
rg "pattern" -l | xargs bat
```
**With fzf:**
```bash
fzf --preview 'bat --color=always --style=numbers {}'
```
**With diff:**
```bash
diff -u file1 file2 | bat -l diff
```
## Configuration
Create `~/.config/bat/config` for defaults:
```
# Set theme
--theme="Dracula"
# Show line numbers, Git modifications and file header, but no grid
--style="numbers,changes,header"
# Use italic text on terminal
--italic-text=always
# Add custom mapping
--map-syntax "*.conf:INI"
```
## Performance Tips
- Use `-p` for plain mode when piping
- Use `--paging=never` when output is used programmatically
- `bat` caches parsed files for faster subsequent access
## Tips
- **Alias:** `alias cat='bat -p'` for drop-in cat replacement
- **Pager:** Use as pager with `export PAGER="bat"`
- **On Debian/Ubuntu:** Command may be `batcat` instead of `bat`
- **Custom syntaxes:** Add to `~/.config/bat/syntaxes/`
- **Performance:** For huge files, use `bat --paging=never` or plain `cat`
## Common flags
- `-p` / `--plain`: Plain mode (no line numbers/decorations)
- `-n` / `--number`: Only show line numbers
- `-A` / `--show-all`: Show non-printable characters
- `-l` / `--language`: Set language for syntax highlighting
- `-r` / `--line-range`: Only show specific line range(s)
## Documentation
GitHub: https://github.com/sharkdp/bat
Man page: `man bat`
Customization: https://github.com/sharkdp/bat#customization
+194
View File
@@ -0,0 +1,194 @@
---
name: fd-find
description: A fast and user-friendly alternative to 'find' - simple syntax, smart defaults, respects gitignore.
homepage: https://github.com/sharkdp/fd
metadata: {"clawdbot":{"emoji":"📂","requires":{"bins":["fd"]},"install":[{"id":"brew","kind":"brew","formula":"fd","bins":["fd"],"label":"Install fd (brew)"},{"id":"apt","kind":"apt","package":"fd-find","bins":["fd"],"label":"Install fd (apt)"}]}}
---
# fd - Fast File Finder
User-friendly alternative to `find` with smart defaults.
## Quick Start
### Basic search
```bash
# Find files by name
fd pattern
# Find in specific directory
fd pattern /path/to/dir
# Case-insensitive
fd -i pattern
```
### Common patterns
```bash
# Find all Python files
fd -e py
# Find multiple extensions
fd -e py -e js -e ts
# Find directories only
fd -t d pattern
# Find files only
fd -t f pattern
# Find symlinks
fd -t l
```
## Advanced Usage
### Filtering
```bash
# Exclude patterns
fd pattern -E "node_modules" -E "*.min.js"
# Include hidden files
fd -H pattern
# Include ignored files (.gitignore)
fd -I pattern
# Search all (hidden + ignored)
fd -H -I pattern
# Maximum depth
fd pattern -d 3
```
### Execution
```bash
# Execute command on results
fd -e jpg -x convert {} {.}.png
# Parallel execution
fd -e md -x wc -l
# Use with xargs
fd -e log -0 | xargs -0 rm
```
### Regex patterns
```bash
# Full regex search
fd '^test.*\.js$'
# Match full path
fd --full-path 'src/.*/test'
# Glob pattern
fd -g "*.{js,ts}"
```
## Time-based filtering
```bash
# Modified within last day
fd --changed-within 1d
# Modified before specific date
fd --changed-before 2024-01-01
# Created recently
fd --changed-within 1h
```
## Size filtering
```bash
# Files larger than 10MB
fd --size +10m
# Files smaller than 1KB
fd --size -1k
# Specific size range
fd --size +100k --size -10m
```
## Output formatting
```bash
# Absolute paths
fd --absolute-path
# List format (like ls -l)
fd --list-details
# Null separator (for xargs)
fd -0 pattern
# Color always/never/auto
fd --color always pattern
```
## Common Use Cases
**Find and delete old files:**
```bash
fd --changed-before 30d -t f -x rm {}
```
**Find large files:**
```bash
fd --size +100m --list-details
```
**Copy all PDFs to directory:**
```bash
fd -e pdf -x cp {} /target/dir/
```
**Count lines in all Python files:**
```bash
fd -e py -x wc -l | awk '{sum+=$1} END {print sum}'
```
**Find broken symlinks:**
```bash
fd -t l -x test -e {} \; -print
```
**Search in specific time window:**
```bash
fd --changed-within 2d --changed-before 1d
```
## Integration with other tools
**With ripgrep:**
```bash
fd -e js | xargs rg "pattern"
```
**With fzf (fuzzy finder):**
```bash
vim $(fd -t f | fzf)
```
**With bat (cat alternative):**
```bash
fd -e md | xargs bat
```
## Performance Tips
- `fd` is typically much faster than `find`
- Respects `.gitignore` by default (disable with `-I`)
- Uses parallel traversal automatically
- Smart case: lowercase = case-insensitive, any uppercase = case-sensitive
## Tips
- Use `-t` for type filtering (f=file, d=directory, l=symlink, x=executable)
- `-e` for extension is simpler than `-g "*.ext"`
- `{}` in `-x` commands represents the found path
- `{.}` strips the extension
- `{/}` gets basename, `{//}` gets directory
## Documentation
GitHub: https://github.com/sharkdp/fd
Man page: `man fd`
+112
View File
@@ -0,0 +1,112 @@
---
name: jq-json-processor
description: Process, filter, and transform JSON data using jq - the lightweight and flexible command-line JSON processor.
homepage: https://jqlang.github.io/jq/
metadata: {"clawdbot":{"emoji":"🔍","requires":{"bins":["jq"]},"install":[{"id":"brew","kind":"brew","formula":"jq","bins":["jq"],"label":"Install jq (brew)"},{"id":"apt","kind":"apt","package":"jq","bins":["jq"],"label":"Install jq (apt)"}]}}
---
# jq JSON Processor
Process, filter, and transform JSON data with jq.
## Quick Examples
### Basic filtering
```bash
# Extract a field
echo '{"name":"Alice","age":30}' | jq '.name'
# Output: "Alice"
# Multiple fields
echo '{"name":"Alice","age":30}' | jq '{name: .name, age: .age}'
# Array indexing
echo '[1,2,3,4,5]' | jq '.[2]'
# Output: 3
```
### Working with arrays
```bash
# Map over array
echo '[{"name":"Alice"},{"name":"Bob"}]' | jq '.[].name'
# Output: "Alice" "Bob"
# Filter array
echo '[1,2,3,4,5]' | jq 'map(select(. > 2))'
# Output: [3,4,5]
# Length
echo '[1,2,3]' | jq 'length'
# Output: 3
```
### Common operations
```bash
# Pretty print JSON
cat file.json | jq '.'
# Compact output
cat file.json | jq -c '.'
# Raw output (no quotes)
echo '{"name":"Alice"}' | jq -r '.name'
# Output: Alice
# Sort keys
echo '{"z":1,"a":2}' | jq -S '.'
```
### Advanced filtering
```bash
# Select with conditions
jq '[.[] | select(.age > 25)]' people.json
# Group by
jq 'group_by(.category)' items.json
# Reduce
echo '[1,2,3,4,5]' | jq 'reduce .[] as $item (0; . + $item)'
# Output: 15
```
### Working with files
```bash
# Read from file
jq '.users[0].name' users.json
# Multiple files
jq -s '.[0] * .[1]' file1.json file2.json
# Modify and save
jq '.version = "2.0"' package.json > package.json.tmp && mv package.json.tmp package.json
```
## Common Use Cases
**Extract specific fields from API response:**
```bash
curl -s https://api.github.com/users/octocat | jq '{name: .name, repos: .public_repos, followers: .followers}'
```
**Convert CSV-like data:**
```bash
jq -r '.[] | [.name, .email, .age] | @csv' users.json
```
**Debug API responses:**
```bash
curl -s https://api.example.com/data | jq '.'
```
## Tips
- Use `-r` for raw string output (removes quotes)
- Use `-c` for compact output (single line)
- Use `-S` to sort object keys
- Use `--arg name value` to pass variables
- Pipe multiple jq operations: `jq '.a' | jq '.b'`
## Documentation
Full manual: https://jqlang.github.io/jq/manual/
Interactive tutorial: https://jqplay.org/
+120
View File
@@ -0,0 +1,120 @@
---
name: nrepl-eval
description: Evaluate Clojure code via nREPL using the standalone tools/nrepl-eval.mjs CLI tool.
---
# nREPL Eval
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
`tools/nrepl-eval.mjs` — a standalone CLI application.
Session state (defs, in-ns, etc.) persists across invocations via a stored
session ID, so you can build up state incrementally.
## Usage
```bash
node tools/nrepl-eval.mjs [options] [<code>]
```
The tool is also executable directly:
```bash
./tools/nrepl-eval.mjs [options] [<code>]
```
## Options
| Flag | Description | Default |
|------|-------------|---------|
| `-p, --port PORT` | nREPL server port | `6064` |
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
| `--reset-session` | Discard stored session and start fresh | — |
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
| `-h, --help` | Show help message | — |
## When to Use
Use this tool when you need to:
1. **Evaluate Clojure code** during development — test functions, inspect
state, or run experiments against a running Clojure process.
2. **Verify that edited files compile** — require namespaces with `:reload`
to pick up changes.
3. **Inspect the last exception** after a failed evaluation — use `-e` to
print the error stored in `*e`.
## Workflow
### 1. Session management
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
carries across calls automatically:
```bash
./tools/nrepl-eval.mjs '(def x 42)'
./tools/nrepl-eval.mjs 'x'
# => 42
```
Reset the session to start fresh:
```bash
./tools/nrepl-eval.mjs --reset-session '(def x 0)'
```
### 2. Evaluate code
**Single expression (inline) — uses default port 6064:**
```bash
./tools/nrepl-eval.mjs '(+ 1 2 3)'
```
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
```bash
./tools/nrepl-eval.mjs <<'EOF'
(def x 10)
(+ x 20)
EOF
```
**Override with a different port:**
```bash
./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
```
### 3. Inspect last exception
After code throws an error, retrieve the full exception details:
```bash
./tools/nrepl-eval.mjs -e
```
## Common Patterns
**Require a namespace with reload:**
```bash
./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
```
**Test a function:**
```bash
./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
```
**Long-running operation with custom timeout:**
```bash
./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)"
```
## Key Principles
- **Default port is 6064** — just pass code directly, no `-p` needed when
your nREPL server is on 6064. Use `-p <PORT>` for a different port.
- **Always use `:reload`** when requiring namespaces to pick up file changes.
- **Session is reused** across invocations — defs, in-ns, and var bindings
persist. Use `--reset-session` to clear.
- **Do not start any server** — the tool connects to an existing nREPL
server, it is not the agent's responsibility to start the nREPL server
(assume the server is already running on the specified port).
+150
View File
@@ -0,0 +1,150 @@
---
name: ripgrep
description: Blazingly fast text search tool - recursively searches directories for regex patterns with respect to gitignore rules.
homepage: https://github.com/BurntSushi/ripgrep
metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["rg"]},"install":[{"id":"brew","kind":"brew","formula":"ripgrep","bins":["rg"],"label":"Install ripgrep (brew)"},{"id":"apt","kind":"apt","package":"ripgrep","bins":["rg"],"label":"Install ripgrep (apt)"}]}}
---
# ripgrep (rg)
Fast, smart recursive search. Respects `.gitignore` by default.
## Quick Start
### Basic search
```bash
# Search for "TODO" in current directory
rg "TODO"
# Case-insensitive search
rg -i "fixme"
# Search specific file types
rg "error" -t py # Python files only
rg "function" -t js # JavaScript files
```
### Common patterns
```bash
# Whole word match
rg -w "test"
# Show only filenames
rg -l "pattern"
# Show with context (3 lines before/after)
rg -C 3 "function"
# Count matches
rg -c "import"
```
## Advanced Usage
### File type filtering
```bash
# Multiple file types
rg "error" -t py -t js
# Exclude file types
rg "TODO" -T md -T txt
# List available types
rg --type-list
```
### Search modifiers
```bash
# Regex search
rg "user_\d+"
# Fixed string (no regex)
rg -F "function()"
# Multiline search
rg -U "start.*end"
# Only show matches, not lines
rg -o "https?://[^\s]+"
```
### Path filtering
```bash
# Search specific directory
rg "pattern" src/
# Glob patterns
rg "error" -g "*.log"
rg "test" -g "!*.min.js"
# Include hidden files
rg "secret" --hidden
# Search all files (ignore .gitignore)
rg "pattern" --no-ignore
```
## Replacement Operations
```bash
# Preview replacements
rg "old_name" --replace "new_name"
# Actually replace (requires extra tool like sd)
rg "old_name" -l | xargs sed -i 's/old_name/new_name/g'
```
## Performance Tips
```bash
# Parallel search (auto by default)
rg "pattern" -j 8
# Skip large files
rg "pattern" --max-filesize 10M
# Memory map files
rg "pattern" --mmap
```
## Common Use Cases
**Find TODOs in code:**
```bash
rg "TODO|FIXME|HACK" --type-add 'code:*.{rs,go,py,js,ts}' -t code
```
**Search in specific branches:**
```bash
git show branch:file | rg "pattern"
```
**Find files containing multiple patterns:**
```bash
rg "pattern1" | rg "pattern2"
```
**Search with context and color:**
```bash
rg -C 2 --color always "error" | less -R
```
## Comparison to grep
- **Faster:** Typically 5-10x faster than grep
- **Smarter:** Respects `.gitignore`, skips binary files
- **Better defaults:** Recursive, colored output, line numbers
- **Easier:** Simpler syntax for common tasks
## Tips
- `rg` is often faster than `grep -r`
- Use `-t` for file type filtering instead of `--include`
- Combine with other tools: `rg pattern -l | xargs tool`
- Add custom types in `~/.ripgreprc`
- Use `--stats` to see search performance
## Documentation
GitHub: https://github.com/BurntSushi/ripgrep
User Guide: https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md
-40
View File
@@ -1,40 +0,0 @@
dist: xenial
language: generic
sudo: required
cache:
directories:
- $HOME/.m2
services:
- docker
branches:
only:
- master
- develop
install:
- curl -O https://download.clojure.org/install/linux-install-1.10.1.447.sh
- chmod +x linux-install-1.10.1.447.sh
- sudo ./linux-install-1.10.1.447.sh
before_script:
- env | sort
script:
- ./manage.sh build-devenv
- ./manage.sh run-frontend-tests
- ./manage.sh run-backend-tests
- ./manage.sh build-images
- ./manage.sh run
after_script:
- docker images
notifications:
email: false
env:
- NODE_VERSION=10.16.0
-11
View File
@@ -1,11 +0,0 @@
enableGlobalCache: true
enableImmutableCache: false
enableImmutableInstalls: false
enableTelemetry: false
httpTimeout: 600000
nodeLinker: node-modules
+104
View File
@@ -0,0 +1,104 @@
# AI Agent Guide
This document provides the core context and operating guidelines for AI agents
working in this repository.
## Before You Start
Before responding to any user request, you must:
1. Read this file completely.
2. Identify which modules are affected by the task.
3. Load the `AGENTS.md` file **only** for each affected module (see the
architecture table below). Not all modules have an `AGENTS.md` — verify the
file exists before attempting to read it.
4. Do **not** load `AGENTS.md` files for unrelated modules.
## Role: Senior Software Engineer
You are a high-autonomy Senior Full-Stack Software Engineer. You have full
permission to navigate the codebase, modify files, and execute commands to
fulfill your tasks. Your goal is to solve complex technical tasks with high
precision while maintaining a strong focus on maintainability and performance.
### Operational Guidelines
1. Before writing code, describe your plan. If the task is complex, break it
down into atomic steps.
2. Be concise and autonomous.
3. Do **not** touch unrelated modules unless the task explicitly requires it.
4. Commit only when explicitly asked. Follow the commit format rules in
`CONTRIBUTING.md`.
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
`.gitignore` by default.
## Changelogs
The project has two changelogs:
- **Main project changelog**: `CHANGES.md` (root of the repository). Tracks changes for the core Penpot application (backend, frontend, common, render-wasm, exporter, mcp).
- **Plugins changelog**: `plugins/CHANGELOG.md`. Tracks changes for the plugins subproject only.
When making changes, add a changelog entry to the appropriate file under the
`## <version> (Unreleased)` section in the correct category
(`:sparkles: New features & Enhancements` or `:bug: Bugs fixed`).
## GitHub Operations
To obtain the list of repository members/collaborators:
```bash
gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login'
```
To obtain the list of open PRs authored by members:
```bash
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
($members | split("|")) as $m |
.[] | select(.author.login as $a | $m | index($a)) |
"\(.number)\t\(.author.login)\t\(.title)"
'
```
To obtain the list of open PRs from external contributors (non-members):
```bash
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
($members | split("|")) as $m |
.[] | select(.author.login as $a | $m | index($a) | not) |
"\(.number)\t\(.author.login)\t\(.title)"
'
```
## Architecture Overview
Penpot is an open-source design tool composed of several modules:
| Directory | Language | Purpose | Has `AGENTS.md` |
|-----------|----------|---------|:----------------:|
| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | Yes |
| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | Yes |
| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | Yes |
| `render-wasm/` | Rust -> WebAssembly | High-performance canvas renderer (Skia) | Yes |
| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | No |
| `mcp/` | TypeScript | Model Context Protocol integration | No |
| `plugins/` | TypeScript | Plugin runtime and example plugins | No |
Some submodules use `pnpm` workspaces. The root `package.json` and
`pnpm-lock.yaml` manage shared dependencies. Helper scripts live in `scripts/`.
### Module Dependency Graph
```
frontend ──> common
backend ──> common
exporter ──> common
frontend ──> render-wasm (loads compiled WASM)
```
`common` is referenced as a local dependency (`{:local/root "../common"}`) by
both `frontend` and `backend`. Changes to `common` can therefore affect multiple
modules — test across consumers when modifying shared code.
+255 -9
View File
@@ -1,22 +1,264 @@
# CHANGELOG
## 2.13.0 (Unreleased)
## 2.15.2
### :bug: Bugs fixed
- Fix mcp related internal config for docker images [Github #9565](https://github.com/penpot/penpot/pull/9565)
## 2.15.1
### :sparkles: New features & Enhancements
- Add support for chunked uploading of fonts [Github #9560](https://github.com/penpot/penpot/issues/9560)
## 2.15.0
### :sparkles: New features & Enhancements
- Add MCP server integration [Github #9174](https://github.com/penpot/penpot/issues/9174)
(PR: [#9032](https://github.com/penpot/penpot/pull/9032), [#9321](https://github.com/penpot/penpot/pull/9321))
- Add chunked upload API for large media and binary files (removes previous upload size limits) [Github #9516](https://github.com/penpot/penpot/issues/9516)
(PR: [#8909](https://github.com/penpot/penpot/pull/8909))
- Add anonymous telemetry event collection [Github #9467](https://github.com/penpot/penpot/issues/9467)
(PR: [#9065](https://github.com/penpot/penpot/pull/9065), [#9483](https://github.com/penpot/penpot/pull/9483))
- Improve team name validation [Github #9517](https://github.com/penpot/penpot/issues/9517)
(PR: [#9176](https://github.com/penpot/penpot/pull/9176))
- Enhance readability of applied tokens in plugins API [Github #9175](https://github.com/penpot/penpot/issues/9175)
(PR: [#8607](https://github.com/penpot/penpot/pull/8607))
- Encourage use of flex/grid layouts in designs generated via MCP [Github #9081](https://github.com/penpot/penpot/issues/9081)
(PR: [#9084](https://github.com/penpot/penpot/pull/9084))
- Improve MCP server logging, adding Loki support [Github #9415](https://github.com/penpot/penpot/issues/9415)
(PR: [#9425](https://github.com/penpot/penpot/pull/9425))
- Add security headers to Nginx on Docker images [Github #9519](https://github.com/penpot/penpot/issues/9519)
(PR: [#9473](https://github.com/penpot/penpot/pull/9473))
### :bug: Bugs fixed
- Fix text edition mode not exited when changing selection, blocking token application [Github #9346](https://github.com/penpot/penpot/issues/9346)
(PR: [#9355](https://github.com/penpot/penpot/pull/9355))
- Reduce memory usage of MCP server when handling images (by @opcode81) [Github #9420](https://github.com/penpot/penpot/issues/9420)
(PR: [#9431](https://github.com/penpot/penpot/pull/9431))
- Fix Plugin API token methods rejecting JS array of strings (by @boskodev790) [Github #9162](https://github.com/penpot/penpot/issues/9162)
(PR: [#9166](https://github.com/penpot/penpot/pull/9166))
- Fix release notes modal appearing behind the dashboard sidebar (by @RenzoMXD) [Github #8296](https://github.com/penpot/penpot/issues/8296)
(PR: [#9126](https://github.com/penpot/penpot/pull/9126), [#9233](https://github.com/penpot/penpot/pull/9233))
- Fix empty warning on login [Github #9520](https://github.com/penpot/penpot/issues/9520)
(PR: [#9056](https://github.com/penpot/penpot/pull/9056))
- Fix maximum call stack size exceeded in SSE read-stream [Github #9470](https://github.com/penpot/penpot/issues/9470)
(PR: [#9484](https://github.com/penpot/penpot/pull/9484))
- Fix incorrect handling of version restore operation [Github #9515](https://github.com/penpot/penpot/issues/9515)
(PR: [#9041](https://github.com/penpot/penpot/pull/9041))
- Fix MCP ReplServer binding to all interfaces (0.0.0.0) instead of localhost, allowing unauthenticated RCE [Github #9518](https://github.com/penpot/penpot/issues/9518)
(PR: [#9400](https://github.com/penpot/penpot/pull/9400))
- Fix MCP integrations URL copy action to match the URL displayed in settings [Github #9238](https://github.com/penpot/penpot/issues/9238)
(PR: [#9239](https://github.com/penpot/penpot/pull/9239))
- Fix swapped analytics event names on MCP tab-switch dialog (by @Dexterity104) [Github #9496](https://github.com/penpot/penpot/issues/9496)
(PR: [#9322](https://github.com/penpot/penpot/pull/9322))
- Fix multiple selection on shapes with token applied to stroke color [Github #9522](https://github.com/penpot/penpot/issues/9522)
(PR: [#9110](https://github.com/penpot/penpot/pull/9110))
- Fix onboarding modals appearing behind libraries and templates panel [Github #9521](https://github.com/penpot/penpot/issues/9521)
(PR: [#9178](https://github.com/penpot/penpot/pull/9178))
- Fix keep-alive interval leak in PluginBridge (by @opcode81) [Github #9430](https://github.com/penpot/penpot/issues/9430)
(PR: [#9435](https://github.com/penpot/penpot/pull/9435))
## 2.14.5
### :bug: Bugs fixed
- Fix incorrect invitation token handling on register process [Github #9380](https://github.com/penpot/penpot/pull/9380)
## 2.14.4
### :bug: Bugs fixed
- Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006)
- Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122)
- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927)
## 2.14.3
### :sparkles: New features & Enhancements
- Add webp export format to plugin types [Github #8870](https://github.com/penpot/penpot/pull/8870)
- Use shared singleton containers for React portals to reduce DOM growth [Github #8957](https://github.com/penpot/penpot/pull/8957)
### :bug: Bugs fixed
- Fix variants corner cases with selrect and points [Github #8882](https://github.com/penpot/penpot/pull/8882)
- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962)
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
- Fix highlight on frames after rename [Github #8938](https://github.com/penpot/penpot/pull/8938)
- Fix TypeError in sd-token-uuid when resolving tokens interactively [Github #8929](https://github.com/penpot/penpot/pull/8929)
- Fix path drawing preview passing shape instead of content to next-node
- Fix swapped arguments in CLJS PathData `-nth` with default
- Normalize PathData coordinates to safe integer bounds on read
- Fix RangeError from re-entrant error handling causing stack overflow [Github #8962](https://github.com/penpot/penpot/pull/8962)
- Fix builder bool styles and media validation [Github #8963](https://github.com/penpot/penpot/pull/8963)
- Fix "Move to" menu allowing same project as target when multiple files are selected
- Fix crash when index query param is duplicated in URL
- Fix wrong extremity point in path `calculate-extremities` for line-to segments
- Fix reversed args in DTCG shadow composite token conversion
- Fix `inside-layout?` passing shape id instead of shape to `frame-shape?`
- Fix wrong `mapcat` call in `collect-main-shapes`
- Fix stale accumulator in `get-children-in-instance` recursion
- Fix typo `:podition` in swap-shapes grid cell
- Fix multiple selection on shapes with token applied to stroke color
## 2.14.2
### :sparkles: New features & Enhancements
- Add protection for stale JS asset cache to force reload on version mismatch [Github #8638](https://github.com/penpot/penpot/pull/8638)
- Normalize newsletter opt-in checkbox across different register flows [Github #8839](https://github.com/penpot/penpot/pull/8839)
### :bug: Bugs fixed
- Fix PathData corruption root causes across WASM and CLJS (unsafe transmute and byteOffset handling)
- Handle corrupted PathData segments gracefully instead of crashing
- Fix swapped move-to/line-to type codes in PathData binary readers
- Fix non-integer row/column values in grid cell position inputs [Github #8869](https://github.com/penpot/penpot/pull/8869)
- Fix nil path content crash by exposing safe public API [Github #8806](https://github.com/penpot/penpot/pull/8806)
- Fix infinite recursion in get-frame-ids for thumbnail extraction [Github #8807](https://github.com/penpot/penpot/pull/8807)
- Fix stale-asset detector missing protocol-dispatch errors
- Ignore Zone.js toString TypeError in uncaught error handler [Github #8804](https://github.com/penpot/penpot/pull/8804)
- Prevent thumbnail frame recursion overflow [Github #8763](https://github.com/penpot/penpot/pull/8763)
- Fix vector index out of bounds in viewer zoom-to-fit/fill [Github #8834](https://github.com/penpot/penpot/pull/8834)
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
## 2.14.1
### :sparkles: New features & Enhancements
- Add automatic retry with backoff for idempotent RPC requests on network failures [Github #8792](https://github.com/penpot/penpot/pull/8792)
- Add scroll and zoom throttling to one state update per animation frame [Github #8812](https://github.com/penpot/penpot/pull/8812)
- Improve error handling and exception formatting [Github #8757](https://github.com/penpot/penpot/pull/8757)
### :bug: Bugs fixed
- Fix crash in apply-text-modifier with nil selrect or modifier [Github #8762](https://github.com/penpot/penpot/pull/8762)
- Fix incorrect attrs references on generate-sync-shape [Github #8776](https://github.com/penpot/penpot/pull/8776)
- Fix regression on subpath support [Github #8793](https://github.com/penpot/penpot/pull/8793)
- Improve error reporting on request parsing failures [Github #8805](https://github.com/penpot/penpot/pull/8805)
- Fix fetch abort errors escaping the unhandled exception handler [Github #8801](https://github.com/penpot/penpot/pull/8801)
- Fix nil deref on missing bounds in layout modifier propagation [Github #8735](https://github.com/penpot/penpot/pull/8735)
- Fix TypeError when token error map lacks :error/fn key [Github #8767](https://github.com/penpot/penpot/pull/8767)
- Fix dissoc error when detaching stroke color from library [Github #8738](https://github.com/penpot/penpot/pull/8738)
- Fix crash when pasting image into text editor
- Fix null text crash on paste in text editor
- Ensure path content is always PathData when saving
- Fix error when get-parent-with-data encounters non-Element nodes
## 2.14.0
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`.
### :sparkles: New features & Enhancements
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
- Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
### :bug: Bugs fixed
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
- Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513)
- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528)
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
- Fix incorrect query for file versions [Github #8463](https://github.com/penpot/penpot/pull/8463)
- Fix warning when clicking on number token pills [Taiga #13661](https://tree.taiga.io/project/penpot/issue/13661)
- Fix 'not ISeqable' error when entering float values in layout item and opacity inputs [Github #8569](https://github.com/penpot/penpot/pull/8569)
- Fix crash in select component when options vector is empty [Github #8578](https://github.com/penpot/penpot/pull/8578)
- Fix scroll on colorpicker [Taiga #13623](https://tree.taiga.io/project/penpot/issue/13623)
- Fix crash when pasting non-map transit clipboard data [Github #8580](https://github.com/penpot/penpot/pull/8580)
- Fix `penpot.openPage()` plugin API not navigating in the same tab; change default to same-tab navigation and allow passing a UUID string instead of a Page object [Github #8520](https://github.com/penpot/penpot/issues/8520)
## 2.13.3
### :bug: Bugs fixed
- Revert yetti (http server) update, because that caused a regression on multipart uploads
## 2.13.2
### :bug: Bugs fixed
- Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317)
- Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)
## 2.13.1
### :bug: Bugs fixed
- Fix PDF Exporter outputs empty page when board has A4 format [Taiga #13181](https://tree.taiga.io/project/penpot/issue/13181)
## 2.13.0
### :heart: Community contributions (Thank you!)
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
### :sparkles: New features & Enhancements
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
- Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997)
- Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951)
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167)
- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
## 2.12.1
## 2.12.0 (Unreleased)
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0
### :boom: Breaking changes & Deprecations
@@ -27,7 +269,6 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH.
#### Updated SSO Callback URL
The OAuth / Single Sign-On (SSO) callback endpoint has changed to
@@ -60,7 +301,6 @@ This update standardizes all authentication flows under the single URL
and makis it more modular, enabling the ability to configure SSO auth
provider dinamically.
#### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small
@@ -77,6 +317,8 @@ example. It's still usable as before, we just removed the example.
### :heart: Community contributions (Thank you!)
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
- Enable Hindi translations on the application
### :sparkles: New features & Enhancements
@@ -92,10 +334,8 @@ example. It's still usable as before, we just removed the example.
### :bug: Bugs fixed
- Fix text line-height values are wrong [Taiga #12252](https://tree.taiga.io/project/penpot/issue/12252)
- Fix an error translation [Taiga #12402](https://tree.taiga.io/project/penpot/issue/12402)
- Fix pan cursor not disabling viewport guides [Github #6985](https://github.com/penpot/penpot/issues/6985)
- Fix viewport resize on locked shapes [Taiga #11974](https://tree.taiga.io/project/penpot/issue/11974)
- Fix nested variant in a component doesn't keep inherited overrides [Taiga #12299](https://tree.taiga.io/project/penpot/issue/12299)
- Fix on copy instance inside a components chain touched are missing [Taiga #12371](https://tree.taiga.io/project/penpot/issue/12371)
- Fix problem with multiple selection and shadows [Github #7437](https://github.com/penpot/penpot/issues/7437)
- Fix search shortcut [Taiga #10265](https://tree.taiga.io/project/penpot/issue/10265)
@@ -104,6 +344,13 @@ example. It's still usable as before, we just removed the example.
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
- Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843)
- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966)
## 2.11.1
@@ -115,7 +362,6 @@ example. It's still usable as before, we just removed the example.
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`
@@ -170,7 +416,7 @@ example. It's still usable as before, we just removed the example.
- Fix text override is lost after switch [Taiga #12269](https://tree.taiga.io/project/penpot/issue/12269)
- Fix exporting a board crashing the app [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12384)
- Fix nested variant in a component doesn't keep inherited overrides [Taiga #12299](https://tree.taiga.io/project/penpot/issue/12299)
- Fix selected colors not showing colors from children shapes in multiple selection [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12385)
- Fix selected colors not showing colors from children shapes in multiple selection [Taiga #12385](https://tree.taiga.io/project/penpot/issue/12385)
- Fix scrollbar issue in design tab [Taiga #12367](https://tree.taiga.io/project/penpot/issue/12367)
- Fix library update notificacions showing when they should not [Taiga #12397](https://tree.taiga.io/project/penpot/issue/12397)
- Fix remove flex button doesnt work within variant [Taiga #12314](https://tree.taiga.io/project/penpot/issue/12314)
+237 -161
View File
@@ -1,213 +1,289 @@
# Contributing Guide #
# Contributing Guide
Thank you for your interest in contributing to Penpot. This is a
generic guide that details how to contribute to the project in a way that
is efficient for everyone. If you are looking for specific documentation on
different parts of the platform, please refer to the `docs/` directory,
or the rendered version at the [Help Center](https://help.penpot.app/).
Thank you for your interest in contributing to Penpot. This guide covers
how to propose changes, submit fixes, and follow project conventions.
## Reporting Bugs ##
For architecture details, module-specific guidelines, and AI-agent
instructions, see [AGENTS.md](AGENTS.md). For final user technical
documentation, see the `docs/` directory or the rendered [Help
Center](https://help.penpot.app/).
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
for our public bugs. We keep a close eye on them and try to make it
clear when we have an internal fix in progress. Before filing a new
task, try to make sure your problem doesn't already exist.
## Table of Contents
If you found a bug, please report it, as far as possible, with:
- [Prerequisites](#prerequisites)
- [Reporting Bugs](#reporting-bugs)
- [Pull Requests](#pull-requests)
- [Workflow](#workflow)
- [Title format](#title-format)
- [Description](#description)
- [Branch naming](#branch-naming)
- [Review process](#review-process)
- [What we won't accept](#what-we-wont-accept)
- [Good first issues](#good-first-issues)
- [Commit Guidelines](#commit-guidelines)
- [Commit types](#commit-types)
- [Rules](#rules)
- [Examples](#examples)
- [Formatting and Linting](#formatting-and-linting)
- [Changelog](#changelog)
- [Code of Conduct](#code-of-conduct)
- [Developer's Certificate of Origin (DCO)](#developers-certificate-of-origin-dco)
- a detailed explanation of steps to reproduce the error
- the browser and browser version used
- a dev tools console exception stack trace (if available)
## Prerequisites
If you found a bug which you think is better to discuss in private (for
example, security bugs), consider first sending an email to
`support@penpot.app`.
- **Language**: Penpot is written primarily in Clojure (backend), ClojureScript
(frontend/exporter), and Rust (render-wasm). Familiarity with the Clojure
ecosystem is expected for most contributions.
- **Issue tracker**: We use [GitHub Issues](https://github.com/penpot/penpot/issues)
for public bugs and [Taiga](https://tree.taiga.io/project/penpot/) for
internal project management. Changelog entries reference both.
**We don't have a formal bug bounty program for security reports; this
is an open source application, and your contribution will be recognized
in the changelog.**
## Reporting Bugs
Report bugs via [GitHub Issues](https://github.com/penpot/penpot/issues).
Before filing, search existing issues to avoid duplicates.
## Pull Requests ##
Include the following when possible:
If you want to propose a change or bug fix via a pull request (PR),
you should first carefully read the section **Developer's Certificate of
Origin**. You must also format your code and commits according to the
instructions below.
1. Steps to reproduce the error.
2. Browser and browser version used.
3. DevTools console exception stack trace (if available).
If you intend to fix a bug, it's fine to submit a pull request right
away, but we still recommend filing an issue detailing what you're
fixing. This is helpful in case we don't accept that specific fix but
want to keep track of the issue.
For security bugs or issues better discussed in private, email
`support@penpot.app` or report them on [Github Security
Advisories](https://github.com/penpot/penpot/security/advisories)
If you want to implement or start working on a new feature, please
open a **question*- / **discussion*- issue for it. No PR
will be accepted without a prior discussion about the changes,
whether it is a new feature, an already planned one, or a quick win.
> **Note:** We do not have a formal bug bounty program. Security
> contributions are recognized in the changelog.
If it is your first PR, you can learn how to proceed from
[this free video
series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
## Pull Requests
We use the `easy fix` tag to indicate issues that are appropriate for beginners.
### Workflow
## Commit Guidelines ##
1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco)
below. All code patches must include a `Signed-off-by` line.
2. **Discuss before building** — open a [GitHub
Issue](https://github.com/penpot/penpot/issues) or start a [GitHub
Discussion](https://github.com/penpot/penpot/discussions) before starting
work on a new feature or significant change. For planned features on the
roadmap, reference the corresponding Taiga story. No PR will be accepted
without prior discussion, whether it is a new feature, a planned one, or a
quick win.
3. **Bug fixes** — you may submit a PR directly, but we still recommend
filing an issue first so we can track it independently of your fix.
4. **Format and lint** — run the checks described in
[Formatting and Linting](#formatting-and-linting) before submitting.
We have very precise rules on how our git commit messages must be formatted.
### Title format
The commit message format is:
Pull request titles **must** follow the same convention as commit subjects:
```
<type> <subject>
:emoji: <subject>
```
- Use the **imperative mood** (e.g. "Fix", not "Fixed").
- Capitalize the first letter of the subject.
- Do not end the subject with a period.
- Keep the subject to **70 characters** or fewer.
- Use one of the [commit type emojis](#commit-types) listed below.
When a PR contains multiple unrelated commits, choose the emoji that
best represents the dominant change.
**Examples:**
```
:bug: Fix unexpected error on launching modal
:sparkles: Enable new modal for profile
:zap: Improve performance of dashboard navigation
```
> **Note:** When a PR is squash-merged, the PR title becomes the
> commit message on the main branch. Getting the title right matters.
### Description
Every pull request should include a description that helps reviewers
understand the change quickly:
1. **What and why** — describe the change and its motivation.
2. **Link related issues** — use `Closes #1234` or reference a Taiga
story (e.g. `Taiga #5678`).
3. **Screenshots or recordings** — required for any UI-visible change.
4. **Testing notes** — how did you verify the change? Any edge cases?
5. **Breaking changes** — call out anything that affects existing users
or requires migration steps.
### Branch naming
Use a descriptive branch name that reflects the type and scope of the
change:
```
<type>/<short-description>
```
Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`.
Optionally include the issue number:
```
fix/9122-email-blacklisting
feat/export-webp
refactor/layout-sizing
```
### Review process
- Maintainers review PRs when time permits. Please be patient.
- Address review feedback by **pushing new commits** — do not
force-push during review, as it breaks comment threads.
- PRs require at least **one approval** before merge.
- We use **squash-merge** by default. The PR title becomes the final
commit message, so follow the [title format](#title-format) above.
### What we won't accept
To save time on both sides, please avoid submitting PRs that:
- Introduce new dependencies without prior discussion.
- Change the build system or CI configuration without maintainer
approval.
- Mix unrelated changes in a single PR — keep PRs focused on one
concern.
- Skip the [discussion step](#workflow) for non-bug-fix changes.
### Good first issues
We use the `easy fix` label to mark issues appropriate for newcomers.
## Commit Guidelines
Commit messages must follow this format:
```
:emoji: <subject>
[body]
[footer]
```
Where type is:
### Commit types
- :bug: `:bug:` a commit that fixes a bug
- :sparkles: `:sparkles:` a commit that adds an improvement
- :tada: `:tada:` a commit with a new feature
- :recycle: `:recycle:` a commit that introduces a refactor
- :lipstick: `:lipstick:` a commit with cosmetic changes
- :ambulance: `:ambulance:` a commit that fixes a critical bug
- :books: `:books:` a commit that improves or adds documentation
- :construction: `:construction:` a WIP commit
- :boom: `:boom:` a commit with breaking changes
- :wrench: `:wrench:` a commit for config updates
- :zap: `:zap:` a commit with performance improvements
- :whale: `:whale:` a commit for Docker-related stuff
- :paperclip: `:paperclip:` a commit with other non-relevant changes
- :arrow_up: `:arrow_up:` a commit with dependency updates
- :arrow_down: `:arrow_down:` a commit with dependency downgrades
- :fire: `:fire:` a commit that removes files or code
- :globe_with_meridians: `:globe_with_meridians:` a commit that adds or updates
translations
| Emoji | Description |
|-------|-------------|
| :bug: | Bug fix |
| :sparkles: | Improvement or enhancement |
| :tada: | New feature |
| :recycle: | Refactor |
| :lipstick: | Cosmetic changes |
| :ambulance: | Critical bug fix |
| :books: | Documentation |
| :construction: | Work in progress |
| :boom: | Breaking change |
| :wrench: | Configuration update |
| :zap: | Performance improvement |
| :whale: | Docker-related change |
| :paperclip: | Other non-relevant changes |
| :arrow_up: | Dependency update |
| :arrow_down: | Dependency downgrade |
| :fire: | Removal of code or files |
| :globe_with_meridians: | Add or update translations |
| :rocket: | Epic or highlight |
More info:
### Rules
- https://gist.github.com/parmentf/035de27d6ed1dce0b36a
- https://gist.github.com/rxaviers/7360908
- Use the **imperative mood** in the subject (e.g. "Fix", not "Fixed")
- Capitalize the first letter of the subject
- Add clear and concise description on the body
- Do not end the subject with a period
- Keep the subject to **70 characters** or fewer
- Separate the subject from the body with a **blank line**
Each commit should have:
### Examples
- A concise subject using the imperative mood.
- The subject should capitalize the first letter, omit the period
at the end, and be no longer than 65 characters.
- A blank line between the subject line and the body.
- An entry in the CHANGES.md file if applicable, referencing the
GitHub or Taiga issue/user story using these same rules.
Examples of good commit messages:
- `:bug: Fix unexpected error on launching modal`
- `:bug: Set proper error message on generic error`
- `:sparkles: Enable new modal for profile`
- `:zap: Improve performance of dashboard navigation`
- `:wrench: Update default backend configuration`
- `:books: Add more documentation for authentication process`
- `:ambulance: Fix critical bug on user registration process`
- `:tada: Add new approach for user registration`
## Formatting and Linting ##
You will want to make sure your code is formatted and linted before submitting
a PR. We use [cljfmt](https://github.com/weavejester/cljfmt) and
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for this. After installing
them on your system, you can run them with:
```bash
# Check formatting
yarn fmt:clj:check
# Check and fix formatting
yarn fmt:clj
# Run the linter
yarn lint:clj
```
:bug: Fix unexpected error on launching modal
:sparkles: Enable new modal for profile
:zap: Improve performance of dashboard navigation
:ambulance: Fix critical bug on user registration process
:tada: Add new approach for user registration
```
There are more choices in `package.json`.
## Formatting and Linting
Ideally, you should run these commands as git pre-commit hooks. A convenient way
of defining them is to use [Husky](https://typicode.github.io/husky/#/).
We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting.
## Code of Conduct ##
```bash
# Check formatting (does not modify files)
./scripts/check-fmt
As contributors and maintainers of this project, we pledge to respect
all people who contribute through reporting issues, posting feature
requests, updating documentation, submitting pull requests or patches,
and other activities.
# Fix formatting (modifies files in place)
./scripts/fmt
We are committed to making participation in this project a
harassment-free experience for everyone, regardless of level of
experience, gender, gender identity and expression, sexual
orientation, disability, personal appearance, body size, race,
ethnicity, age, or religion.
# Lint
./scripts/lint
```
Examples of unacceptable behavior by participants include the use of
sexual language or imagery, derogatory comments or personal attacks,
trolling, public or private harassment, insults, or other
unprofessional conduct.
Ideally, run these as git pre-commit hooks.
[Husky](https://typicode.github.io/husky/#/) is a convenient option for
setting this up.
Project maintainers have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned with this Code of Conduct. Project
maintainers who do not follow the Code of Conduct may be removed from
the project team.
## Changelog
This Code of Conduct applies both within project spaces and in public
spaces when an individual is representing the project or its
community.
When your change is user-facing or otherwise notable, add an entry to
[CHANGES.md](CHANGES.md) following the same commit-type conventions. Reference
the relevant GitHub issue or Taiga user story.
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by opening an issue or contacting one or more of the
project maintainers.
## Code of Conduct
This Code of Conduct is adapted from the Contributor Covenant, version
1.1.0, available from [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
This project follows the [Contributor Covenant](https://www.contributor-covenant.org/).
The full Code of Conduct is available at
[help.penpot.app/contributing-guide/coc](https://help.penpot.app/contributing-guide/coc/)
and in the repository's [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
To report unacceptable behavior, open an issue or contact a project maintainer
directly.
## Developer's Certificate of Origin (DCO)
By submitting code you agree to and can certify the following:
Developer's Certificate of Origin 1.1
> **Developer's Certificate of Origin 1.1**
>
> By making a contribution to this project, I certify that:
>
> (a) The contribution was created in whole or in part by me and I have the
> right to submit it under the open source license indicated in the file; or
>
> (b) The contribution is based upon previous work that, to the best of my
> knowledge, is covered under an appropriate open source license and I have
> the right under that license to submit that work with modifications,
> whether created in whole or in part by me, under the same open source
> license (unless I am permitted to submit under a different license), as
> indicated in the file; or
>
> (c) The contribution was provided directly to me by some other person who
> certified (a), (b) or (c) and I have not modified it.
>
> (d) I understand and agree that this project and the contribution are public
> and that a record of the contribution (including all personal information
> I submit with it, including my sign-off) is maintained indefinitely and
> may be redistributed consistent with this project or the open source
> license(s) involved.
By making a contribution to this project, I certify that:
### Signed-off-by
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
Then, all your code patches (**documentation is excluded**) should
contain a sign-off at the end of the patch/commit description body. It
can be automatically added by adding the `-s` parameter to `git commit`.
This is an example of what the line should look like:
All code patches (**documentation is excluded**) must contain a sign-off line
at the end of the commit body. Add it automatically with `git commit -s`.
```
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Signed-off-by: Your Real Name <your.email@example.com>
```
Please, use your real name (sorry, no pseudonyms or anonymous
contributions are allowed).
- Use your **real name** pseudonyms and anonymous contributions are not
allowed.
- The `Signed-off-by` line is **mandatory** and must match the commit author.
+27 -1
View File
@@ -2,4 +2,30 @@
## Reporting a Vulnerability
Please report security issues to `support@penpot.app`
We take the security of this project seriously. If you have discovered
a security vulnerability, please do **not** open a public issue.
Please report vulnerabilities via email to: **[support@penpot.app]**
### What to include:
* A brief description of the vulnerability.
* Steps to reproduce the issue.
* Potential impact if exploited.
We appreciate your patience and your commitment to **responsible disclosure**.
---
## Security Contributors
We are incredibly grateful to the following individuals and
organizations for their help in keeping this project safe.
* **Ali Maharramli** for identifying critical path traversal vulnerability
> **Note:** This list is a work in progress. If you have contributed
> to the security of this project and would like to be recognized (or
> prefer to remain anonymous), please let us know.
-7
View File
@@ -1,7 +0,0 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+262
View File
@@ -0,0 +1,262 @@
# Penpot Backend Agent Instructions
Clojure backend (RPC) service running on the JVM.
Uses Integrant for dependency injection, PostgreSQL for storage, and
Redis for messaging/caching.
## General Guidelines
To ensure consistency across the Penpot JVM stack, all contributions must adhere
to these criteria.
IMPORTANT: all CLI commands should be executed under backend/
subdirectory for make them work correctly.
### 1. Testing & Validation
* **Coverage:** If code is added or modified in `src/`, corresponding
tests in `test/backend_tests/` must be added or updated.
* **Execution:**
* **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace.
* **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas.
### 2. Code Quality & Formatting
* **Linting:** All code must pass linter checks (run `pnpm run lint:clj` or `pnpm run lint` on the repository root)
* **Formatting:** All the code must pass the formatting check (run `pnpm run
check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty"
diffs caused by unrelated whitespace changes.
* **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in
performance-critical paths to avoid reflection overhead.
## Code Conventions
### Namespace Overview
The source is located under `src` directory and this is a general overview of
namespaces structure:
- `app.rpc.commands.*` RPC command implementations (`auth`, `files`, `teams`, etc.)
- `app.http.*` HTTP routes and middleware
- `app.db.*` Database layer
- `app.tasks.*` Background job tasks
- `app.main` Integrant system setup and entrypoint
- `app.loggers` Internal loggers (auditlog, mattermost, etc.) (not to be confused with `app.common.logging`)
### RPC
The RPC methods are implemented using a multimethod-like structure via the
`app.util.services` namespace. The main RPC methods are collected under
`app.rpc.commands` namespace and exposed under `/api/rpc/command/<cmd-name>`.
The RPC method accepts POST and GET requests indistinctly and uses the `Accept`
header to negotiate the response encoding (which can be Transit — the default —
or plain JSON). It also accepts Transit (default) or JSON as input, which should
be indicated using the `Content-Type` header.
The main convention is: use `get-` prefix on RPC name when we want READ
operation.
Example of RPC method definition:
```clojure
(sv/defmethod ::my-command
{::rpc/auth true ;; requires auth
::doc/added "1.18"
::sm/params [:map ...] ;; malli input schema
::sm/result [:map ...]} ;; malli output schema
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
;; return a plain map or throw
{:id (uuid/next)})
```
Look under `src/app/rpc/commands/*.clj` to see more examples.
### Tests
Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`.
### Integrant System
The `src/app/main.clj` declares the system map. Each key is a component; values
are config maps with `::ig/ref` for dependencies. Components implement
`ig/init-key` / `ig/halt-key!`.
### Connecting to the Database
Two PostgreSQL databases are used in this environment:
| Database | Purpose | Connection string |
|---------------|--------------------|----------------------------------------------------|
| `penpot` | Development / app | `postgresql://penpot:penpot@postgres/penpot` |
| `penpot_test` | Test suite | `postgresql://penpot:penpot@postgres/penpot_test` |
**Interactive psql session:**
```bash
# development DB
psql "postgresql://penpot:penpot@postgres/penpot"
# test DB
psql "postgresql://penpot:penpot@postgres/penpot_test"
```
**One-shot query (non-interactive):**
```bash
psql "postgresql://penpot:penpot@postgres/penpot" -c "SELECT id, name FROM team LIMIT 5;"
```
**Useful psql meta-commands:**
```
\dt -- list all tables
\d <table> -- describe a table (columns, types, constraints)
\di -- list indexes
\q -- quit
```
> **Migrations table:** Applied migrations are tracked in the `migrations` table
> with columns `module`, `step`, and `created_at`. When renaming a migration
> logical name, update this table in both databases to match the new name;
> otherwise the runner will attempt to re-apply the migration on next startup.
```bash
# Example: fix a renamed migration entry in the test DB
psql "postgresql://penpot:penpot@postgres/penpot_test" \
-c "UPDATE migrations SET step = 'new-name' WHERE step = 'old-name';"
```
### Database Access (Clojure)
`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.
```clojure
;; Query helpers
(db/get cfg-or-pool :table {:id id}) ; fetch one row (throws if missing)
(db/get* cfg-or-pool :table {:id id}) ; fetch one row (returns nil)
(db/query cfg-or-pool :table {:team-id team-id}) ; fetch multiple rows
(db/insert! cfg-or-pool :table {:name "x" :team-id id}) ; insert
(db/update! cfg-or-pool :table {:name "y"} {:id id}) ; update
(db/delete! cfg-or-pool :table {:id id}) ; delete
;; Run multiple statements/queries on single connection
(db/run! cfg (fn [{:keys [::db/conn]}]
(db/insert! conn :table row1)
(db/insert! conn :table row2))
;; Transactions
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
(db/insert! conn :table row)))
```
Almost all methods in the `app.db` namespace accept `pool`, `conn`, or
`cfg` as params.
Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup.
### Error Handling
The exception helpers are defined on Common module, and are available under
`app.common.exceptions` namespace.
Example of raising an exception:
```clojure
(ex/raise :type :not-found
:code :object-not-found
:hint "File does not exist"
:file-id id)
```
Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`.
### Performance Macros (`app.common.data.macros`)
Always prefer these macros over their `clojure.core` equivalents — they provide
optimized implementations:
```clojure
(dm/select-keys m [:a :b]) ;; faster than core/select-keys
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
(dm/str "a" "b" "c") ;; string concatenation
```
### Configuration
`src/app/config.clj` reads `PENPOT_*` environment variables, validated with
Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags
:enable-smtp)`.
### Background Tasks
Background tasks live in `src/app/tasks/`. Each task is an Integrant component
that exposes a `::handler` key and follows this three-method pattern:
```clojure
(defmethod ig/assert-key ::handler ;; validate config at startup
[_ params]
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
(defmethod ig/expand-key ::handler ;; inject defaults before init
[k v]
{k (assoc v ::my-option default-value)})
(defmethod ig/init-key ::handler ;; return the task fn
[_ cfg]
(fn [_task] ;; receives the task row from the worker
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
;; … do work …
))))
```
**Wiring a new task** requires two changes in `src/app/main.clj`:
1. **Handler config** add an entry in `system-config` with the dependencies:
```clojure
:app.tasks.my-task/handler
{::db/pool (ig/ref ::db/pool)}
```
2. **Registry + cron** register the handler name and schedule it:
```clojure
;; in ::wrk/registry ::wrk/tasks map:
:my-task (ig/ref :app.tasks.my-task/handler)
;; in worker-config ::wrk/cron ::wrk/entries vector:
{:cron #penpot/cron "0 0 0 * * ?" ;; daily at midnight
:task :my-task}
```
**Useful cron patterns** (Quartz format — six fields: s m h dom mon dow):
| Expression | Meaning |
|------------------------------|--------------------|
| `"0 0 0 * * ?"` | Daily at midnight |
| `"0 0 */6 * * ?"` | Every 6 hours |
| `"0 */5 * * * ?"` | Every 5 minutes |
**Time helpers** (`app.common.time`):
```clojure
(ct/now) ;; current instant
(ct/duration {:hours 1}) ;; java.time.Duration
(ct/minus (ct/now) some-duration) ;; subtract duration from instant
```
`db/interval` converts a `Duration` (or millis / string) to a PostgreSQL
interval object suitable for use in SQL queries:
```clojure
(db/interval (ct/duration {:hours 1})) ;; → PGInterval "3600.0 seconds"
```
+8 -8
View File
@@ -3,7 +3,7 @@
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/clojure {:mvn/version "1.12.4"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.8"
:git/sha "1d1b33f"
{:git/tag "v11.9"
:git/sha "5fad7a9"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}
@@ -39,7 +39,7 @@
metosin/reitit-core {:mvn/version "0.9.1"}
nrepl/nrepl {:mvn/version "1.4.0"}
org.postgresql/postgresql {:mvn/version "42.7.7"}
org.postgresql/postgresql {:mvn/version "42.7.9"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
@@ -49,7 +49,7 @@
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.3"}
org.jsoup/jsoup {:mvn/version "1.21.2"}
org.im4java/im4java
@@ -66,7 +66,7 @@
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
software.amazon.awssdk/s3 {:mvn/version "2.41.21"}}
:paths ["src" "resources" "target/classes"]
:aliases
@@ -97,8 +97,8 @@
:jmx-remote
{:jvm-opts ["-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.port=9090"
"-Dcom.sun.management.jmxremote.rmi.port=9090"
"-Dcom.sun.management.jmxremote.port=9000"
"-Dcom.sun.management.jmxremote.rmi.port=9000"
"-Dcom.sun.management.jmxremote.local.only=false"
"-Dcom.sun.management.jmxremote.authenticate=false"
"-Dcom.sun.management.jmxremote.ssl=false"
+5 -4
View File
@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c",
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
@@ -19,8 +19,9 @@
"ws": "^8.17.0"
},
"scripts": {
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"lint:clj": "clj-kondo --parallel --lint src/"
"lint": "clj-kondo --parallel --lint ../common/src src/",
"check-fmt": "cljfmt check --parallel=true src/ test/",
"fmt": "cljfmt fix --parallel=true src/ test/",
"test": "clojure -M:dev:test"
}
}
+306
View File
@@ -0,0 +1,306 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
luxon:
specifier: ^3.4.4
version: 3.7.2
sax:
specifier: ^1.4.1
version: 1.4.3
devDependencies:
nodemon:
specifier: ^3.1.2
version: 3.1.11
source-map-support:
specifier: ^0.5.21
version: 0.5.21
ws:
specifier: ^8.17.0
version: 8.18.3
packages:
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nodemon@3.1.11:
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
engines: {node: '>=10'}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
touch@3.1.1:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
snapshots:
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
braces@3.0.3:
dependencies:
fill-range: 7.1.1
buffer-from@1.1.2: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
concat-map@0.0.1: {}
debug@4.4.3(supports-color@5.5.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 5.5.0
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
fsevents@2.3.3:
optional: true
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
has-flag@3.0.0: {}
ignore-by-default@1.0.1: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
luxon@3.7.2: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
ms@2.1.3: {}
nodemon@3.1.11:
dependencies:
chokidar: 3.6.0
debug: 4.4.3(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.7.3
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
undefsafe: 2.0.5
normalize-path@3.0.0: {}
picomatch@2.3.1: {}
pstree.remy@1.1.8: {}
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
sax@1.4.3: {}
semver@7.7.3: {}
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.3
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
touch@3.1.1: {}
undefsafe@2.0.5: {}
ws@8.18.3: {}
View File
Whitespace-only changes.
@@ -240,4 +240,4 @@
</div>
</body>
</html>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
{:id "penpot-design-system"
:name "Penpot Design System | Pencil"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/penpot-app.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Pencil-Penpot-Design-System.penpot"}
{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
@@ -5,7 +5,6 @@
<meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style>
{% include "app/templates/styles.css" %}
</style>
+93 -79
View File
@@ -12,43 +12,22 @@ Debug Main Page
</nav>
<main class="dashboard">
<section class="widget">
<fieldset>
<legend>Error reports</legend>
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
<fieldset>
<legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification">
<div class="row">
<input type="email" name="email" placeholder="example@example.com" value="" />
</div>
<div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="resend" value="Resend Verification" />
<input type="submit" name="verify" value="Verify" />
</div>
<div class="row">
<input type="submit" class="danger" name="block" value="Block" />
<input type="submit" class="danger" name="unblock" value="Unblock" />
</div>
</form>
<legend>CURRENT PROFILE</legend>
<desc>
<p>
Name: <b>{{profile.fullname}}</b> <br />
Email: <b>{{profile.email}}</b>
</p>
</desc>
</fieldset>
<fieldset>
<legend>VIRTUAL CLOCK</legend>
<desc>
<p><b>IMPORTANT:</b> The virtual clock is profile based and only affects the currently logged-in profile.</p>
<p>
CURRENT CLOCK: <b>{{current-clock}}</b>
<br />
@@ -81,8 +60,93 @@ Debug Main Page
</form>
</fieldset>
<fieldset>
<legend>ERROR REPORTS</legend>
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
</section>
<section class="widget">
<fieldset>
<legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification">
<div class="row">
<input type="email" name="email" placeholder="example@example.com" value="" />
</div>
<div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="resend" value="Resend Verification" />
<input type="submit" name="verify" value="Verify" />
</div>
<div class="row">
<input type="submit" class="danger" name="block" value="Block" />
<input type="submit" class="danger" name="unblock" value="Unblock" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<fieldset>
@@ -173,55 +237,5 @@ Debug Main Page
</form>
</fieldset>
</section>
<section class="widget">
<fieldset>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
</main>
{% endblock %}
+22 -19
View File
@@ -5,23 +5,26 @@ penpot - error list
{% endblock %}
{% block content %}
<nav>
<div class="title">
<h1>Error reports (last 200)
<a href="/dbg">[GO BACK]</a>
</h1>
</div>
</nav>
<main class="horizontal-list">
<ul>
{% for item in items %}
<li>
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
<a class="hint" href="/dbg/error/{{item.id}}">
<span class="title">{{item.hint|abbreviate:150}}</span>
</a>
</li>
{% endfor %}
</ul>
</main>
<nav>
<div class="title">
<a href="/dbg"> [BACK]</a>
<h1>Error reports (last 300)</h1>
<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 = 5 %}strong{% endif %}" href="?version=5">[RLIMIT REPORTS]</a>
</div>
</nav>
<main class="horizontal-list">
<ul>
{% for item in items %}
<li>
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
<a class="hint" href="/dbg/error/{{item.id}}">
<span class="title">{{item.hint|abbreviate:150}}</span>
</a>
</li>
{% endfor %}
</ul>
</main>
{% endblock %}
@@ -6,7 +6,7 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
{% block content %}
<nav>
<div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<div>[<a href="#props">props</a>]</div>
<div>[<a href="#context">context</a>]</div>
@@ -0,0 +1,46 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
{% 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>
{% if report %}
<div>[<a href="#report">report</a>]</div>
{% endif %}
</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">Origin:</span> <br/> {{origin}}</h2>
<h2><span class="not-important">HREF:</span> <br/> {{href}}</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>
{% if report %}
<div class="table-row multiline">
<div id="report" class="table-key">REPORT:</div>
<div class="table-val">
<pre>{{report}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}
@@ -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 %}
+19 -3
View File
@@ -1,5 +1,5 @@
* {
font-family: "JetBrains Mono", monospace;
font-family: monospace;
font-size: 12px;
}
@@ -36,6 +36,10 @@ small {
color: #888;
}
.strong {
font-weight: 900;
}
.not-important {
color: #888;
font-weight: 200;
@@ -57,14 +61,26 @@ nav {
nav > .title {
display: flex;
justify-content: center;
width: 100%;
}
nav > .title > a {
color: black;
text-decoration: none;
}
nav > .title > a.strong {
text-decoration: underline;
}
nav > .title > h1 {
padding: 0px;
margin: 0px;
font-size: 11px;
display: block;
}
nav > .title > * {
padding: 0px 6px;
}
nav > div {
+2 -2
View File
@@ -3,9 +3,9 @@
{:default
[[:default :window "200000/h"]]
;; #{:command/get-teams}
;; #{:main/get-teams}
;; [[:burst :bucket "5/5/5s"]]
;; #{:command/get-profile}
;; #{:main/get-profile}
;; [[:burst :bucket "60/60/1m"]]
}
+16 -2
View File
@@ -1,17 +1,25 @@
#!/usr/bin/env bash
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
# DEPRECATED: only used for subscriptions
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_HOST=devenv
export PENPOT_PUBLIC_URI=https://localhost:3449
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-login-with-password
enable-login-with-password \
disable-login-with-ldap \
disable-login-with-oidc \
disable-login-with-google \
disable-login-with-github \
disable-login-with-gitlab \
disable-telemetry \
enable-backend-worker \
enable-backend-asserts \
disable-feature-fdata-pointer-map \
@@ -37,6 +45,10 @@ export PENPOT_FLAGS="\
enable-redis-cache \
enable-subscriptions";
# Uncomment for nexus integration testing
# export PENPOT_FLAGS="$PENPOT_FLAGS enable-audit-log-archive";
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
@@ -54,6 +66,8 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/control-center
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
+4
View File
@@ -3,6 +3,10 @@
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config
setup_minio;
+5
View File
@@ -3,6 +3,11 @@
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
export OPTIONS="-A:dev"
entrypoint=${1:-app.main};
+4
View File
@@ -3,6 +3,10 @@
SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config
setup_minio;
+19 -30
View File
@@ -36,17 +36,6 @@
[integrant.core :as ig]
[yetti.response :as-alias yres]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OIDC PROVIDER (GENERIC)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -54,7 +43,7 @@
(defn- discover-oidc-config
[cfg {:keys [base-uri] :as provider}]
(let [uri (u/join base-uri ".well-known/openid-configuration")
rsp (http/req! cfg {:method :get :uri (dm/str uri)})]
rsp (http/req cfg {:method :get :uri (dm/str uri)})]
(if (= 200 (:status rsp))
(let [data (-> rsp :body json/decode)
@@ -116,7 +105,7 @@
(defn- fetch-oidc-jwks
[cfg jwks-uri]
(let [{:keys [status body]} (http/req! cfg {:method :get :uri jwks-uri})]
(let [{:keys [status body]} (http/req cfg {:method :get :uri jwks-uri})]
(if (= 200 status)
(-> body json/decode :keys process-oidc-jwks)
(ex/raise :type ::internal
@@ -177,7 +166,7 @@
(l/inf :hint "provider initialized"
:provider (:id provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider)))
:client-secret (d/obfuscate-string (:client-secret provider)))
provider)
(catch Throwable cause
@@ -222,7 +211,7 @@
(l/inf :hint "provider initialized"
:provider (:id provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider)))
:client-secret (d/obfuscate-string (:client-secret provider)))
provider)
(catch Throwable cause
@@ -246,7 +235,7 @@
:timeout 6000
:method :get}
{:keys [status body]} (http/req! cfg params)]
{:keys [status body]} (http/req cfg params)]
(when-not (int-in-range? status 200 300)
(ex/raise :type :internal
@@ -299,7 +288,7 @@
(l/inf :hint "provider initialized"
:provider (:id provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider)))
:client-secret (d/obfuscate-string (:client-secret provider)))
provider)
(catch Throwable cause
@@ -341,7 +330,7 @@
:provider "gitlab"
:base-uri (:base-uri provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider)))
:client-secret (d/obfuscate-string (:client-secret provider)))
provider)
(catch Throwable cause
(ex/raise :type ::internal
@@ -361,7 +350,7 @@
(l/inf :hint "provider initialized"
:provider (:id provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider)))
:client-secret (d/obfuscate-string (:client-secret provider)))
provider)
(catch Throwable cause
@@ -434,7 +423,7 @@
(defn- qualify-prop-key
[provider k]
(keyword (:type provider) (name k)))
(keyword (:type provider) (-> k name str/kebab)))
(defn- qualify-props
[provider props]
@@ -459,11 +448,11 @@
(l/trc :hint "fetch access token"
:provider (:id provider)
:client-id (:client-id provider)
:client-secret (obfuscate-string (:client-secret provider))
:client-secret (d/obfuscate-string (:client-secret provider))
:grant-type (:grant_type params)
:redirect-uri (:redirect_uri params))
(let [{:keys [status body]} (http/req! cfg req)]
(let [{:keys [status body]} (http/req cfg req)]
(if (= status 200)
(let [data (json/decode body)
data {:token/access (get data :access_token)
@@ -512,13 +501,13 @@
[cfg provider tdata]
(l/trc :hint "fetch user info"
:uri (:user-uri provider)
:token (obfuscate-string (:token/access tdata)))
:token (d/obfuscate-string (:token/access tdata)))
(let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
:timeout 6000
:method :get}
response (http/req! cfg params)]
response (http/req cfg params)]
(l/trc :hint "user info response"
:status (:status response)
@@ -815,12 +804,12 @@
props (audit/profile->props profile)
context (d/without-nils {:external-session-id (:external-session-id info)})]
(audit/submit! cfg {::audit/type "action"
::audit/name "login-with-oidc"
::audit/profile-id (:id profile)
::audit/ip-addr (inet/parse-request request)
::audit/props props
::audit/context context})
(audit/submit cfg {:type "action"
:name "login-with-oidc"
:profile-id (:id profile)
:ip-addr (inet/parse-request request)
:props props
:context context})
(->> (redirect-to-verify-token token)
(sxf request)))))
+75
View File
@@ -331,6 +331,81 @@
(set/difference cfeat/backend-only-features))
#{}))))
(defn check-file-exists
[cfg id & {:keys [include-deleted?]
:or {include-deleted? false}
:as options}]
(db/get-with-sql cfg [sql:get-minimal-file id]
{:db/remove-deleted (not include-deleted?)}))
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn- get-file-permissions*
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-file-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions* conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-file-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(defn get-project
[cfg project-id]
(db/get cfg :project {:id project-id}))
+1 -1
View File
@@ -40,8 +40,8 @@
[promesa.util :as pu]
[yetti.adapter :as yt])
(:import
com.github.luben.zstd.ZstdIOException
com.github.luben.zstd.ZstdInputStream
com.github.luben.zstd.ZstdIOException
com.github.luben.zstd.ZstdOutputStream
java.io.DataInputStream
java.io.DataOutputStream
+26 -12
View File
@@ -281,7 +281,7 @@
thumbnails (bfc/get-file-object-thumbnails cfg file-id)]
(events/tap :progress {:section :file :id file-id})
(events/tap :progress {:section :file :id file-id :name (:name file)})
(vswap! bfc/*state* update :files assoc file-id
{:id file-id
@@ -301,6 +301,7 @@
(write-entry! output path file))
(doseq [[index page-id] (d/enumerate pages)]
(let [path (str "files/" file-id "/pages/" page-id ".json")
page (get pages-index page-id)
objects (:objects page)
@@ -311,6 +312,8 @@
(write-entry! output path page)
(events/tap :progress {:section :page :id page-id :name (:name page) :file-id file-id})
(doseq [[shape-id shape] objects]
(let [path (str "files/" file-id "/pages/" page-id "/" shape-id ".json")
shape (assoc shape :page-id page-id)
@@ -323,6 +326,8 @@
(doseq [{:keys [id] :as media} media]
(let [path (str "files/" file-id "/media/" id ".json")
media (encode-media media)]
(events/tap :progress {:section :media :id id :file-id file-id})
(write-entry! output path media)))
(doseq [thumbnail thumbnails]
@@ -332,11 +337,13 @@
data (-> data
(assoc :media-id (:media-id thumbnail))
(encode-file-thumbnail))]
(events/tap :progress {:section :thumbnails :id (:object-id thumbnail) :file-id file-id})
(write-entry! output path data)))
(doseq [[id component] components]
(let [path (str "files/" file-id "/components/" id ".json")
component (encode-component component)]
(events/tap :progress {:section :component :id id :file-id file-id})
(write-entry! output path component)))
(doseq [[id color] colors]
@@ -347,17 +354,20 @@
(and (contains? color :path)
(str/empty? (:path color)))
(dissoc :path))]
(events/tap :progress {:section :color :id id :file-id file-id})
(write-entry! output path color)))
(doseq [[id object] typographies]
(let [path (str "files/" file-id "/typographies/" id ".json")
typography (encode-typography object)]
(events/tap :progress {:section :typography :id id :file-id file-id})
(write-entry! output path typography)))
(when (and tokens-lib
(not (ctob/empty-lib? tokens-lib)))
(let [path (str "files/" file-id "/tokens.json")
encoded-tokens (encode-tokens-lib tokens-lib)]
(events/tap :progress {:section :tokens-lib :file-id file-id})
(write-entry! output path encoded-tokens)))))
(defn- export-files
@@ -600,6 +610,7 @@
(let [object (->> (read-entry input entry)
(decode-color)
(validate-color))]
(events/tap :progress {:section :color :id id :file-id file-id})
(if (= id (:id object))
(assoc result id object)
result)))
@@ -631,6 +642,7 @@
(clean-component-pre-decode)
(decode-component)
(clean-component-post-decode))]
(events/tap :progress {:section :component :id id :file-id file-id})
(if (= id (:id object))
(assoc result id object)
result)))
@@ -644,6 +656,7 @@
(let [object (->> (read-entry input entry)
(decode-typography)
(validate-typography))]
(events/tap :progress {:section :typography :id id :file-id file-id})
(if (= id (:id object))
(assoc result id object)
result)))
@@ -653,6 +666,7 @@
(defn- read-file-tokens-lib
[{:keys [::bfc/input ::entries]} file-id]
(when-let [entry (d/seek (match-tokens-lib-entry-fn file-id) entries)]
(events/tap :progress {:section :tokens-lib :file-id file-id})
(->> (read-plain-entry input entry)
(decode-tokens-lib)
(validate-tokens-lib))))
@@ -678,6 +692,7 @@
(let [page (->> (read-entry input entry)
(decode-page))
page (dissoc page :options)]
(events/tap :progress {:section :page :id id :file-id file-id})
(when (= id (:id page))
(let [objects (read-file-shapes cfg file-id id)]
(assoc page :objects objects))))))
@@ -693,6 +708,7 @@
(let [object (->> (read-entry input entry)
(decode-file-thumbnail)
(validate-file-thumbnail))]
(if (and (= frame-id (:frame-id object))
(= page-id (:page-id object))
(= tag (:tag object)))
@@ -733,8 +749,6 @@
(vswap! bfc/*state* update :index bfc/update-index media :id)
(events/tap :progress {:section :media :file-id file-id})
(doseq [item media]
(let [params (-> item
(update :id bfc/lookup-index)
@@ -742,6 +756,8 @@
(d/update-when :media-id bfc/lookup-index)
(d/update-when :thumbnail-id bfc/lookup-index))]
(events/tap :progress {:section :media :id (:id params) :file-id file-id})
(l/dbg :hint "inserting media object"
:file-id (str file-id')
:id (str (:id params))
@@ -753,8 +769,6 @@
(db/insert! conn :file-media-object params
::db/on-conflict-do-nothing? (::bfc/overwrite cfg))))
(events/tap :progress {:section :thumbnails :file-id file-id})
(doseq [item thumbnails]
(let [media-id (bfc/lookup-index (:media-id item))
object-id (-> (assoc item :file-id file-id')
@@ -769,6 +783,8 @@
:media-id (str media-id)
::l/sync? true)
(events/tap :progress {:section :thumbnail :file-id file-id :object-id object-id})
(db/insert! conn :file-tagged-object-thumbnail params
::db/on-conflict-do-nothing? true)))
@@ -821,9 +837,10 @@
entries (keep (match-storage-entry-fn) entries)]
(doseq [{:keys [id entry]} entries]
(let [object (->> (read-entry input entry)
(decode-storage-object)
(validate-storage-object))
(let [object (-> (read-entry input entry)
(decode-storage-object)
(update :bucket d/nilv sto/default-bucket)
(validate-storage-object))
ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext)
@@ -872,11 +889,8 @@
(import-storage-objects cfg)
(let [files (get manifest :files)
result (reduce (fn [result {:keys [id] :as file}]
result (reduce (fn [result file]
(let [name' (get file :name)
name' (if (map? name)
(get name id)
name')
file (assoc file :name name')]
(conj result (import-file cfg file))))
[]
+32 -8
View File
@@ -72,6 +72,7 @@
:telemetry-uri "https://telemetry.penpot.app/"
:media-max-file-size (* 1024 1024 30) ; 30MiB
:font-max-file-size (* 1024 1024 30) ; 30MiB
:ldap-user-query "(|(uid=:username)(mail=:username))"
:ldap-attrs-username "uid"
@@ -82,7 +83,14 @@
:initial-project-skey "initial-project"
;; time to avoid email sending after profile modification
:email-verify-threshold "15m"})
:email-verify-threshold "15m"
:quotes-upload-sessions-per-profile 5
:quotes-upload-chunks-per-session 20
;; SSRF protection
:ssrf-allowed-hosts #{}
:ssrf-extra-blocked-cidrs #{}})
(def schema:config
(do #_sm/optional-keys
@@ -98,10 +106,12 @@
[:http-server-port {:optional true} ::sm/int]
[:http-server-host {:optional true} :string]
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:nexus-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string]
@@ -111,6 +121,7 @@
[:auto-file-snapshot-timeout {:optional true} ::ct/duration]
[:media-max-file-size {:optional true} ::sm/int]
[:font-max-file-size {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::ct/duration]
[:file-clean-delay {:optional true} ::ct/duration]
[:telemetry-enabled {:optional true} ::sm/boolean]
@@ -152,6 +163,8 @@
[:quotes-snapshots-per-team {:optional true} ::sm/int]
[:quotes-team-access-requests-per-team {:optional true} ::sm/int]
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
[:quotes-upload-sessions-per-profile {:optional true} ::sm/int]
[:quotes-upload-chunks-per-session {:optional true} ::sm/int]
[:auth-token-cookie-name {:optional true} :string]
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
@@ -225,6 +238,8 @@
[:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int]
[:nitrate-backend-uri {:optional true} ::sm/uri]
;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string]
@@ -236,17 +251,26 @@
[:objects-storage-fs-directory {:optional true} :string]
[:objects-storage-s3-bucket {:optional true} :string]
[:objects-storage-s3-region {:optional true} :keyword]
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]]))
[:objects-storage-s3-endpoint {:optional true} ::sm/uri]
;; SSRF protection
[:ssrf-allowed-hosts {:optional true} [::sm/set :string]]
[:ssrf-extra-blocked-cidrs {:optional true} [::sm/set :string]]]))
(defn- parse-flags
[config]
(let [public-uri (c/get config :public-uri)
public-uri (some-> public-uri (u/uri))
extra-flags (if (and public-uri
(= (:scheme public-uri) "http")
(not= (:host public-uri) "localhost"))
#{:disable-secure-session-cookies}
#{})]
extra-flags (cond-> #{}
;; When public-uri is http (non-localhost), disable secure cookies
(and public-uri
(= (:scheme public-uri) "http")
(not= (:host public-uri) "localhost"))
(conj :disable-secure-session-cookies)
;; When telemetry-enabled config is true, add :telemetry flag
(true? (c/get config :telemetry-enabled))
(conj :enable-telemetry))]
(flags/parse flags/default extra-flags (:flags config))))
(defn read-env
+1 -1
View File
@@ -36,11 +36,11 @@
java.sql.Connection
java.sql.PreparedStatement
java.sql.Savepoint
org.postgresql.PGConnection
org.postgresql.geometric.PGpoint
org.postgresql.jdbc.PgArray
org.postgresql.largeobject.LargeObject
org.postgresql.largeobject.LargeObjectManager
org.postgresql.PGConnection
org.postgresql.util.PGInterval
org.postgresql.util.PGobject))
+27 -10
View File
@@ -22,15 +22,34 @@
[cuerdas.core :as str]
[integrant.core :as ig])
(:import
jakarta.mail.Message$RecipientType
jakarta.mail.Session
jakarta.mail.Transport
jakarta.mail.internet.InternetAddress
jakarta.mail.internet.MimeBodyPart
jakarta.mail.internet.MimeMessage
jakarta.mail.internet.MimeMultipart
jakarta.mail.Message$RecipientType
jakarta.mail.Session
jakarta.mail.Transport
java.util.Properties))
(defn clean
"Clean and normalizes email address string"
[email]
(let [email (str/lower email)
email (if (str/starts-with? email "mailto:")
(subs email 7)
email)
email (if (or (str/starts-with? email "<")
(str/ends-with? email ">"))
(str/trim email "<>")
email)]
email))
(defn get-domain
[email]
(let [email (clean email)
[_ domain] (str/split email "@" 2)]
domain))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; EMAIL IMPL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -106,17 +125,17 @@
(let [content-part (MimeBodyPart.)
alternative-mpart (MimeMultipart. "alternative")]
(when-let [content (get body "text/plain")]
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String content ^String charset)
(.addBodyPart alternative-mpart text-part)))
(when-let [content (get body "text/html")]
(let [html-part (MimeBodyPart.)]
(.setContent html-part ^String content
(str "text/html; charset=" charset))
(.addBodyPart alternative-mpart html-part)))
(when-let [content (get body "text/plain")]
(let [text-part (MimeBodyPart.)]
(.setText text-part ^String content ^String charset)
(.addBodyPart alternative-mpart text-part)))
(.setContent content-part alternative-mpart)
(.addBodyPart mixed-mpart content-part))
@@ -124,8 +143,6 @@
(throw (IllegalArgumentException. "invalid email body provided")))
(doseq [[name content] attachments]
(prn "attachment" name)
(let [attachment-part (MimeBodyPart.)]
(.setFileName attachment-part ^String name)
(.setContent attachment-part ^String content (str "text/plain; charset=" charset))
+11 -3
View File
@@ -36,10 +36,18 @@
:cause cause)))))
(defn contains?
"Check if email is in the blacklist."
"Check if email is in the blacklist. Also matches subdomains: if
'somedomain.com' is blacklisted, 'xxx@foo.somedomain.com' will also
be rejected."
[{:keys [::email/blacklist]} email]
(let [[_ domain] (str/split email "@" 2)]
(c/contains? blacklist (str/lower domain))))
(let [[_ domain] (str/split email "@" 2)
parts (str/split (str/lower domain) #"\.")]
(loop [parts parts]
(if (empty? parts)
false
(if (c/contains? blacklist (str/join "." parts))
true
(recur (rest parts)))))))
(defn enabled?
"Check if the blacklist is enabled"
+2 -2
View File
@@ -138,6 +138,7 @@
c.deleted_at
FROM snapshots1 AS c
WHERE c.file_id = ?
ORDER BY c.created_at DESC
), snapshots3 AS (
(SELECT * FROM snapshots2
WHERE created_by = 'system'
@@ -150,8 +151,7 @@
AND deleted_at IS NULL
LIMIT 500)
)
SELECT * FROM snapshots3
ORDER BY created_at DESC"))
SELECT * FROM snapshots3;"))
(defn get-visible-snapshots
"Return a list of snapshots fecheable from the API, it has a limited
+3 -4
View File
@@ -42,8 +42,8 @@
(def default-params
{::port 6060
::host "0.0.0.0"
::max-body-size 31457280 ; default 30 MiB
::max-multipart-body-size 367001600}) ; default 350 MiB
::max-body-size 367001600 ; default 350 MiB
})
(defmethod ig/expand-key ::server
[k v]
@@ -56,7 +56,6 @@
[::io-threads {:optional true} ::sm/int]
[::max-worker-threads {:optional true} ::sm/int]
[::max-body-size {:optional true} ::sm/int]
[::max-multipart-body-size {:optional true} ::sm/int]
[::router {:optional true} [:fn r/router?]]
[::handler {:optional true} ::sm/fn]])
@@ -79,7 +78,7 @@
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:http/max-multipart-body-size (::max-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (::io-threads cfg)
:xnio/max-worker-threads (::max-worker-threads cfg)
+43 -15
View File
@@ -12,43 +12,56 @@
[app.common.time :as ct]
[app.common.uri :as u]
[app.db :as db]
[app.http.session :as session]
[app.storage :as sto]
[integrant.core :as ig]
[yetti.response :as-alias yres]))
(def ^:private cache-max-age
(def ^:private default-cache-max-age
(ct/duration {:hours 24}))
(def ^:private signature-max-age
(def ^:private default-signature-max-age
(ct/duration {:hours 24 :minutes 15}))
;; Buckets that are legitimately public and do not require authentication.
;; These are used by public shared board viewing, profile photos in UI,
;; and embedded export/binfile flows.
(def ^:private public-buckets
#{"file-media-object"
"file-object-thumbnail"
"team-font-variant"
"file-data-fragment"})
(defn get-id
[{:keys [path-params]}]
(or (some-> path-params :id d/parse-uuid)
(ex/raise :type :not-found
:hunt "object not found")))
:hint "object not found")))
(defn- get-file-media-object
[pool id]
(db/get pool :file-media-object {:id id}))
(db/get pool :file-media-object {:id id} {::db/remove-deleted false}))
(defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj]
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
[{:keys [::sto/storage ::signature-max-age ::cache-max-age] :as cfg} obj]
(let [sig-max-age (or signature-max-age default-signature-max-age)
cch-max-age (or cache-max-age default-cache-max-age)
{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age sig-max-age})]
{::yres/status 307
::yres/headers {"location" (str url)
"x-host" (cond-> host port (str ":" port))
"x-mtype" (-> obj meta :content-type)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}}))
"cache-control" (str "max-age=" (inst-ms cch-max-age))}}))
(defn- serve-object-from-fs
[{:keys [::path]} obj]
(let [purl (u/join (u/uri path)
[{:keys [::path ::cache-max-age]} obj]
(let [cch-max-age (or cache-max-age default-cache-max-age)
purl (u/join (u/uri path)
(sto/object->relative-path obj))
mdata (meta obj)
headers {"x-accel-redirect" (:path purl)
"content-type" (:content-type mdata)
"cache-control" (str "max-age=" (inst-ms cache-max-age))}]
"cache-control" (str "max-age=" (inst-ms cch-max-age))}]
{::yres/status 204
::yres/headers headers}))
@@ -60,14 +73,28 @@
(:s3 :assets-s3) (serve-object-from-s3 cfg obj)
(:fs :assets-fs) (serve-object-from-fs cfg obj)))
(defn- requires-auth?
"Check if the storage object requires authentication based on its bucket."
[obj]
(let [bucket (-> obj meta :bucket)]
(not (contains? public-buckets bucket))))
(defn objects-handler
"Handler that servers storage objects by id."
"Handler that serves storage objects by id.
For non-public buckets (e.g. profile), requires an authenticated session."
[{:keys [::sto/storage] :as cfg} request]
(let [id (get-id request)
obj (sto/get-object storage id)]
(if obj
(serve-object cfg obj)
{::yres/status 404})))
(cond
(nil? obj)
{::yres/status 404}
(and (requires-auth? obj)
(nil? (::session/profile-id request)))
{::yres/status 401}
:else
(serve-object cfg obj))))
(defn- generic-handler
"A generic handler helper/common code for file-media based handlers."
@@ -96,11 +123,12 @@
(defmethod ig/assert-key ::routes
[_ params]
(assert (sto/valid-storage? (::sto/storage params)) "expected valid storage instance")
(assert (session/manager? (::session/manager params)) "expected valid session manager")
(assert (string? (::path params))))
(defmethod ig/init-key ::routes
[_ cfg]
["/assets"
["/assets" {:middleware [[session/authz cfg]]}
["/by-id/:id" {:handler (partial objects-handler cfg)}]
["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}]
["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]])
+1 -1
View File
@@ -53,7 +53,7 @@
(let [surl (get body "SubscribeURL")
stopic (get body "TopicArn")]
(l/info :action "subscription received" :topic stopic :url surl)
(http/req! cfg {:uri surl :method :post :timeout 10000} {:sync? true}))
(http/req cfg {:uri surl :method :post :timeout 10000} {:sync? true}))
(= mtype "Notification")
(when-let [message (parse-json (get body "Message"))]
+93 -14
View File
@@ -5,13 +5,24 @@
;; Copyright (c) KALEIDOS INC
(ns app.http.client
"Http client abstraction layer."
"Http client abstraction layer.
All outbound requests made through `req` and `req-with-redirects`
are validated against the SSRF blocklist by default. Pass
`:skip-ssrf-check? true` in the options map only when the target
is a well-known, operator-configured endpoint that cannot be
influenced by user input (e.g. internal telemetry, error webhooks)."
(:require
[app.common.schema :as sm]
[app.util.ssrf :as ssrf]
[cuerdas.core :as str]
[integrant.core :as ig]
[java-http-clj.core :as http])
(:import
java.net.http.HttpClient))
java.net.http.HttpClient
java.net.URI))
(def default-max-redirects 5)
(defn client?
[o]
@@ -23,8 +34,8 @@
(defmethod ig/init-key ::client
[_ _]
(http/build-client {:connect-timeout 30000 ;; 10s
:follow-redirects :always}))
(http/build-client {:connect-timeout 30000
:follow-redirects :never}))
(defn send!
([client req] (send! client req {}))
@@ -44,14 +55,82 @@
:else
(throw (UnsupportedOperationException. "invalid arguments"))))
(defn req!
"A convencience toplevel function for gradual migration to a new API
convention."
(defn req
"Issue a single HTTP request. SSRF validation is applied to the
target URI by default; pass `:skip-ssrf-check? true` in `options`
to bypass it for known-safe, operator-configured endpoints."
([cfg-or-client request]
(let [client (resolve-client cfg-or-client)
request (update request :uri str)]
(send! client request {})))
([cfg-or-client request options]
(let [client (resolve-client cfg-or-client)
request (update request :uri str)]
(send! client request options))))
(req cfg-or-client request {}))
([cfg-or-client request {:keys [skip-ssrf-check?] :as options}]
(let [request (if skip-ssrf-check?
(update request :uri str)
(update request :uri ssrf/validate-uri))
client (resolve-client cfg-or-client)]
(send! client request (dissoc options :skip-ssrf-check?)))))
(defn- resolve-location
"Resolve a Location header value against the original request URI.
Handles:
- Absolute URLs (http:// or https://) — returned as-is.
- Protocol-relative URLs (//host/path) — inherit the scheme from base-uri.
- Path-absolute and relative URLs — resolved against base-uri via URI.resolve."
[^String base-uri ^String location]
(cond
(or (str/starts-with? location "http://")
(str/starts-with? location "https://"))
location
(str/starts-with? location "//")
(let [scheme (.getScheme (URI. base-uri))]
(str scheme ":" location))
:else
(str (.resolve (URI. base-uri) location))))
(defn- redirect-request
"Build the next request for a 3xx redirect.
Per RFC 7231 §6.4:
- 303 always issues GET (body dropped).
- 301/302 with non-GET/HEAD methods: downgrade to GET (body dropped).
- 307/308 preserve the original method and body.
The Location URI has already been resolved by the caller."
[orig-request ^String next-uri status]
(let [method (:method orig-request)]
(if (or (= status 303)
(and (contains? #{301 302} status)
(not (contains? #{:get :head} method))))
;; Downgrade to GET, drop body and content-type
(-> orig-request
(assoc :uri next-uri :method :get)
(dissoc :body)
(update :headers dissoc "content-type" "content-length"))
;; Preserve method/body (307, 308, or GET/HEAD 301/302)
(assoc orig-request :uri next-uri))))
(defn req-with-redirects
"Like `req`, but follows up to `max-redirects` HTTP 3xx redirects.
SSRF validation is applied before every hop (initial request and
each redirect target) unless `:skip-ssrf-check? true` is passed.
Redirect semantics follow RFC 7231 §6.4: 301/302 POST is downgraded
to GET; 303 always uses GET; 307/308 preserve the original method."
([cfg-or-client request]
(req-with-redirects cfg-or-client request {}))
([cfg-or-client request {:keys [max-redirects skip-ssrf-check?]
:or {max-redirects default-max-redirects}
:as opts}]
(let [send-opts (dissoc opts :max-redirects :skip-ssrf-check?)
uri-coerce (if skip-ssrf-check? str ssrf/validate-uri)]
(loop [current-req (update request :uri uri-coerce)
hops 0]
(let [client (resolve-client cfg-or-client)
resp (send! client current-req send-opts)
status (:status resp)]
(if (and (<= 300 status 399)
(< hops max-redirects))
(if-let [location (get-in resp [:headers "location"])]
(let [next-uri (resolve-location (str (:uri current-req)) location)]
(recur (update (redirect-request current-req next-uri status) :uri uri-coerce)
(inc hops)))
;; No Location header on a 3xx — return the response as-is
resp)
resp))))))
+39 -16
View File
@@ -49,13 +49,16 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn index-handler
[_cfg _request]
(let [{:keys [clock offset]} @clock/current]
[cfg request]
(let [profile-id (::session/profile-id request)
offset (clock/get-offset profile-id)
profile (profile/get-profile cfg profile-id)]
{::yres/status 200
::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {:version (:full cf/version)
:current-clock (str clock)
:profile profile
:current-clock ct/*clock*
:current-offset (if offset
(ct/format-duration offset)
"NO OFFSET")
@@ -229,13 +232,30 @@
(-> (io/resource "app/templates/error-report.v3.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 3)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
(render-template-v4 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v4.tmpl")
(tmpl/render (-> content
(assoc :id id)
(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))))))]
(if-let [report (get-report request)]
(let [result (case (:version report)
1 (render-template-v1 report)
2 (render-template-v2 report)
3 (render-template-v3 report))]
3 (render-template-v3 report)
4 (render-template-v4 report)
5 (render-template-v5 report))]
{::yres/status 200
::yres/body result
::yres/headers {"content-type" "text/html; charset=utf-8"
@@ -243,20 +263,22 @@
{::yres/status 404
::yres/body "not found"})))
(def sql:error-reports
(def ^:private sql:error-reports
"SELECT id, created_at,
content->>'~:hint' AS hint
FROM server_error_report
WHERE version = ?
ORDER BY created_at DESC
LIMIT 200")
LIMIT 300")
(defn error-list-handler
[{:keys [::db/pool]} _request]
(let [items (->> (db/exec! pool [sql:error-reports])
(map #(update % :created-at ct/format-inst :rfc1123)))]
(defn- error-list-handler
[{:keys [::db/pool]} {:keys [params]}]
(let [version (or (some-> (get params :version) parse-long) 3)
items (->> (db/exec! pool [sql:error-reports version])
(map #(update % :created-at ct/format-inst :rfc1123)))]
{::yres/status 200
::yres/body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items}))
(tmpl/render {:items items :version version}))
::yres/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}}))
@@ -447,15 +469,16 @@
(defn- set-virtual-clock
[_ {:keys [params] :as request}]
(let [offset (some-> params :offset str/trim not-empty ct/duration)
reset? (contains? params :reset)]
(let [offset (some-> params :offset str/trim not-empty ct/duration)
profile-id (::session/profile-id request)
reset? (contains? params :reset)]
(if (= "production" (cf/get :tenant))
{::yres/status 501
::yres/body "OPERATION NOT ALLOWED"}
(do
(if (or reset? (zero? (inst-ms offset)))
(clock/set-offset! nil)
(clock/set-offset! offset))
(clock/assign-offset profile-id nil)
(clock/assign-offset profile-id offset))
{::yres/status 302
::yres/headers {"location" "/dbg"}}))))
@@ -495,7 +518,7 @@
(defn authorized?
[pool {:keys [::session/profile-id]}]
(or (= "devenv" (cf/get :host))
(or (and (= "devenv" (cf/get :host)) profile-id)
(let [profile (ex/ignoring (profile/get-profile pool profile-id))
admins (or (cf/get :admins) #{})]
(contains? admins (:email profile)))))
+9 -7
View File
@@ -32,7 +32,7 @@
(assoc :request/ip-addr (inet/parse-request request))
(assoc :request/profile-id (get claims :uid))
(assoc :request/auth-data auth)
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
(assoc :frontend/version (or (yreq/get-header request "x-frontend-version") "unknown")))))
(defmulti handle-error
(fn [cause _ _]
@@ -220,12 +220,14 @@
(assoc :hint (ex-message error)))}))))
(defmethod handle-exception java.io.IOException
[cause _ _]
(l/wrn :hint "io exception" :cause cause)
{::yres/status 500
::yres/body {:type :server-error
:code :io-exception
:hint (ex-message cause)}})
[cause request _]
(binding [l/*context* (request->context request)]
(l/wrn :hint "io exception" :cause cause)
{::yres/status 500
::yres/body {:type :server-error
:code :io-exception
:hint (ex-message cause)
:path (:path request)}}))
(defmethod handle-exception java.util.concurrent.CompletionException
[cause request _]
+31 -19
View File
@@ -13,13 +13,13 @@
[app.common.time :as ct]
[app.config :as cf]
[app.db :as db]
[app.http.middleware :as mw]
[app.main :as-alias main]
[app.rpc.commands.profile :as cmd.profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[integrant.core :as ig]
[yetti.request :as yreq]
[yetti.response :as-alias yres]))
;; ---- ROUTES
@@ -49,28 +49,40 @@
(fn [cfg request]
(db/tx-run! cfg handler request)))))})
(def ^:private shared-key-auth
{:name ::shared-key-auth
:compile
(fn [_ _]
(fn [handler key]
(if key
(fn [request]
(if-let [key' (yreq/get-header request "x-shared-key")]
(if (= key key')
(handler request)
{::yres/status 403})
{::yres/status 403}))
(fn [_ _]
{::yres/status 403}))))})
(defmethod ig/init-key ::routes
[_ {:keys [::setup/props] :as cfg}]
[_ cfg]
(let [management-key (or (cf/get :management-api-key)
(get props :management-key))]
["" {:middleware [[shared-key-auth (cf/get :management-api-key)]
[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["" {:middleware [[mw/shared-key-auth management-key]
[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]]))
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]])
;; ---- HELPERS
+17 -14
View File
@@ -16,7 +16,6 @@
[app.http.errors :as errors]
[app.tokens :as tokens]
[app.util.pointer-map :as pmap]
[buddy.core.codecs :as bc]
[cuerdas.core :as str]
[yetti.adapter :as yt]
[yetti.middleware :as ymw]
@@ -214,14 +213,14 @@
(assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width")))
(assoc "access-control-expose-headers" "content-type, set-cookie")
(assoc "access-control-allow-headers" "x-frontend-version, x-client, x-requested-width, content-type, accept, cookie")))
(defn wrap-cors
[handler]
(fn [request]
(let [response (if (= (yreq/method request) :options)
{::yres/status 200}
{::yres/status 204}
(handler request))
origin (yreq/get-header request "origin")]
(update response ::yres/headers with-cors-headers origin))))
@@ -301,16 +300,20 @@
:compile (constantly wrap-auth)})
(defn- wrap-shared-key-auth
[handler shared-key]
(if shared-key
(let [shared-key (if (string? shared-key)
shared-key
(bc/bytes->b64-str shared-key true))]
(fn [request]
(let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key)
(handler request)
{::yres/status 403}))))
[handler keys]
(if (seq keys)
(fn [request]
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key")
(str/split #"\s+" 2))]
(let [key-id (-> key-id str/lower keyword)]
(if (and (string? key)
(contains? keys key-id)
(= key (get keys key-id)))
(-> request
(assoc ::http/auth-key-id key-id)
(handler))
{::yres/status 403}))
{::yres/status 403}))
(fn [_ _]
{::yres/status 403})))
+15 -10
View File
@@ -20,6 +20,7 @@
[app.http.session.tasks :as-alias tasks]
[app.main :as-alias main]
[app.setup :as-alias setup]
[app.setup.clock :as clock]
[app.tokens :as tokens]
[integrant.core :as ig]
[yetti.request :as yreq]
@@ -229,18 +230,22 @@
(let [{:keys [type token claims metadata]} (get request ::http/auth-data)]
(cond
(= type :cookie)
(let [session (case (:ver metadata)
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
0 (read-session manager token)
1 (some->> (:sid claims) (read-session manager))
nil)
(let [session
(case (:ver metadata)
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
0 (read-session manager token)
1 (some->> (:sid claims) (read-session manager))
nil)
request (cond-> request
(some? session)
(-> (assoc ::profile-id (:profile-id session))
(assoc ::session session)))
request
(cond-> request
(some? session)
(-> (assoc ::profile-id (:profile-id session))
(assoc ::session session)))
response (handler request)]
response
(binding [ct/*clock* (clock/get-clock (:profile-id session))]
(handler request))]
(if (and session (renew-session? session))
(let [session (->> session
-1
View File
@@ -6,7 +6,6 @@
(ns app.http.sse
"SSE (server sent events) helpers"
(:refer-clojure :exclude [tap])
(:require
[app.common.data :as d]
[app.common.logging :as l]
+297 -160
View File
@@ -9,18 +9,19 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.json :as json]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.email :as email]
[app.http :as-alias http]
[app.http.access-token :as-alias actoken]
[app.loggers.audit.tasks :as-alias tasks]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.retry :as rtry]
[app.setup :as-alias setup]
[app.util.inet :as inet]
[app.util.services :as-alias sv]
@@ -32,6 +33,63 @@
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private filter-auth-events
#{"login-with-oidc" "login-with-password" "register-profile" "update-profile"})
(def ^:private safe-backend-context-keys
#{:version
:initiator
:client-version
:client-user-agent})
(def ^:private safe-frontend-context-keys
#{:version
:locale
:browser
:browser-version
:engine
:engine-version
:os
:os-version
:device-type
:device-arch
:screen-width
:screen-height
:screen-color-depth
:screen-orientation
:event-origin
:event-namespace
:event-symbol})
(def profile-props
[:id
:is-active
:is-muted
:auth-backend
:email
:default-team-id
:default-project-id
:fullname
:lang])
(def ^:private event-keys
#{:id
:name
:type
:profile-id
:ip-addr
:props
:context
:source
:tracked-at
:created-at})
(def reserved-props
#{:session-id
:password
:old-password
:token})
(defn extract-utm-params
"Extracts additional data from params and namespace them under
`penpot` ns."
@@ -46,17 +104,6 @@
(assoc (->> sk str/kebab (keyword "penpot")) v))))]
(reduce-kv process-param {} params)))
(def profile-props
[:id
:is-active
:is-muted
:auth-backend
:email
:default-team-id
:default-project-id
:fullname
:lang])
(defn profile->props
[profile]
(-> profile
@@ -64,12 +111,6 @@
(merge (:props profile))
(d/without-nils)))
(def reserved-props
#{:session-id
:password
:old-password
:token})
(defn clean-props
[props]
(into {}
@@ -79,18 +120,6 @@
(remove #(contains? reserved-props (key %))))
props))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context {:external-session-id (::rpc/external-session-id params)
:external-event-origin (::rpc/external-event-origin params)
:triggered-by (::rpc/handler-name params)}]
{::type "action"
::profile-id (::rpc/profile-id params)
::ip-addr (::rpc/ip-addr params)
::context (d/without-nils context)}))
(defn get-external-session-id
[request]
(when-let [session-id (yreq/get-header request "x-external-session-id")]
@@ -99,13 +128,24 @@
(str/blank? session-id))
session-id)))
(defn- get-external-event-origin
(defn- get-client-event-origin
[request]
(when-let [origin (yreq/get-header request "x-event-origin")]
(when-not (or (> (count origin) 256)
(= origin "null")
(when-not (or (= origin "null")
(str/blank? origin))
origin)))
(str/prune origin 200))))
(defn get-client-user-agent
[request]
(when-let [user-agent (yreq/get-header request "user-agent")]
(str/prune user-agent 500)))
(defn- get-client-version
[request]
(when-let [origin (yreq/get-header request "x-frontend-version")]
(when-not (or (= origin "null")
(str/blank? origin))
(str/prune origin 100))))
;; --- SPECS
@@ -113,19 +153,24 @@
;; COLLECTOR API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private prepare-context-from-request)
;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared.
(def ^:private schema:event
[:map {:title "event"}
[::type ::sm/text]
[::name ::sm/text]
[::profile-id ::sm/uuid]
[::ip-addr {:optional true} ::sm/text]
[::props {:optional true} [:map-of :keyword :any]]
[::context {:optional true} [:map-of :keyword :any]]
[::tracked-at {:optional true} ::ct/inst]
[:map {:title "AuditEvent"}
[:id {:optional true} ::sm/uuid]
[:type ::sm/text]
[:name ::sm/text]
[:profile-id ::sm/uuid]
[:props [:map-of :keyword :any]]
[:context [:map-of :keyword :any]]
[:tracked-at ::ct/inst]
[:created-at ::ct/inst]
[:source ::sm/text]
[:ip-addr {:optional true} ::sm/text]
[::webhooks/event? {:optional true} ::sm/boolean]
[::webhooks/batch-timeout {:optional true} ::ct/duration]
[::webhooks/batch-key {:optional true}
@@ -134,7 +179,159 @@
(def ^:private check-event
(sm/check-fn schema:event))
(defn prepare-event
(def valid-event?
(sm/validator schema:event))
(defn- prepare-context-from-request
"Prepare backend event context from request"
[request]
(let [client-event-origin (get-client-event-origin request)
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
key-id (::http/auth-key-id request)
token-id (::actoken/id request)
token-type (::actoken/type request)]
{:external-session-id session-id
:initiator (or key-id "app")
:access-token-id (some-> token-id str)
:access-token-type (some-> token-type str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version
:version (:full cf/version)}))
(defn- append-audit-entry
[cfg params]
(let [params (-> params
(assoc :id (uuid/next))
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet))
params (select-keys params event-keys)]
(db/insert! cfg :audit-log params)))
(def ^:private xf:filter-telemetry-props
"Transducer that keeps only map entries whose values are UUIDs,
booleans or numbers."
(filter (fn [[k v]]
(and (simple-keyword? k)
(or (uuid? v) (boolean? v) (number? v))))))
(declare filter-telemetry-props)
(declare filter-telemetry-context)
(defn- process-event
[cfg event]
(when (contains? cf/flags :audit-log-logger)
(l/log! ::l/logger "app.audit"
::l/level :info
:profile-id (str (:profile-id event))
:ip-addr (str (:ip-addr event))
:type (:type event)
:name (:name event)
:props (json/encode (:props event) :key-fn json/write-camel-key)
:context (json/encode (:context event) :key-fn json/write-camel-key)))
(when (contains? cf/flags :audit-log)
(append-audit-entry cfg event))
(when (contains? cf/flags :telemetry)
;; NOTE: when both audit-log and telemetry are enabled, events are stored
;; twice: once with full details (above) and once stripped of props and
;; ip-addr, tagged with source="telemetry" so the telemetry task can
;; collect and ship them. The profile-id is preserved (UUIDs are already
;; anonymous random identifiers). Only a safe subset of context fields
;; is kept: initiator, version, client-version and client-user-agent.
;; Timestamps are truncated to day precision to avoid leaking exact event
;; timing.
(let [event (-> event
(filter-telemetry-props)
(filter-telemetry-context)
(update :created-at ct/truncate :days)
(update :tracked-at ct/truncate :days)
(assoc :source "telemetry:backend")
(assoc :ip-addr "0.0.0.0"))]
(append-audit-entry cfg event)))
(when (and (contains? cf/flags :webhooks)
(::webhooks/event? event))
(let [batch-key (::webhooks/batch-key event)
batch-timeout (::webhooks/batch-timeout event)
label (dm/str "rpc:" (:name event))
label (cond
(ifn? batch-key) (dm/str label ":" (batch-key (::rpc/params event)))
(string? batch-key) (dm/str label ":" batch-key)
:else label)
dedupe? (boolean (and batch-key batch-timeout))]
(wrk/submit! (-> cfg
(assoc ::wrk/task :process-webhook-event)
(assoc ::wrk/queue :webhooks)
(assoc ::wrk/max-retries 0)
(assoc ::wrk/delay (or batch-timeout 0))
(assoc ::wrk/dedupe dedupe?)
(assoc ::wrk/label label)
(assoc ::wrk/params (-> event
(d/without-qualified)
(dissoc :source)
(dissoc :context)
(dissoc :ip-addr)
(dissoc :type)))))))
event)
(defn submit*
"A public API, lower-level than submit, assumes all required fields are filled"
[cfg event]
(try
(let [event (check-event event)]
(db/tx-run! cfg process-event event))
(catch Throwable cause
(l/error :hint "unexpected error processing event" :cause cause))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn filter-telemetry-props
[{:keys [source name props type] :as params}]
(cond
(or (and (= source "frontend")
(= type "identify"))
(and (= source "backend")
(filter-auth-events name)))
(let [props' (into {} xf:filter-telemetry-props props)
props' (-> props'
(assoc :lang (:lang props))
(assoc :auth-backend (:auth-backend props))
(assoc :email-domain (email/get-domain (:email props)))
(d/without-nils))]
(assoc params :props props'))
(and (= source "backend")
(= type "trigger")
(= name "instance-start"))
params
(and (= source "frontend")
(= type "action")
(= name "navigate"))
(assoc params :props (select-keys props [:route :file-id :team-id :page-id]))
:else
(let [props (into {} xf:filter-telemetry-props props)]
(assoc params :props props))))
(defn filter-telemetry-context
[{:keys [source context] :as params}]
(let [context (case source
"backend" (select-keys context safe-backend-context-keys)
"frontend" (select-keys context safe-frontend-context-keys)
{})]
(assoc params :context context)))
(defn prepare-rpc-event
[cfg mdata params result]
(let [resultm (meta result)
request (-> params meta ::http/request)
@@ -144,32 +341,32 @@
uuid/zero)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(merge params (::props resultm)))
(clean-props))
token-id (::actoken/id request)
context (-> (::context resultm)
(assoc :external-session-id
(get-external-session-id request))
(assoc :external-event-origin
(get-external-event-origin request))
(assoc :access-token-id (some-> token-id str))
(merge (prepare-context-from-request request))
(assoc :request-id (::rpc/request-id params))
(d/without-nils))
ip-addr (inet/parse-request request)]
ip-addr (inet/parse-request request)
module (get cfg ::rpc/module)]
{::type (or (::type resultm)
(::rpc/type cfg))
::name (or (::name resultm)
(::sv/name mdata))
::profile-id profile-id
::ip-addr ip-addr
::props props
::context context
{:type (or (::type resultm)
(::rpc/type cfg))
:name (or (::name resultm)
(let [sname (::sv/name mdata)]
(if (not= module "main")
(str module "-" sname)
sname)))
:profile-id profile-id
:ip-addr ip-addr
:props props
:context context
:created-at (::rpc/request-at params)
:tracked-at (::rpc/request-at params)
;; NOTE: for batch-key lookup we need the params as-is
;; because the rpc api does not need to know the
@@ -189,109 +386,49 @@
(::webhooks/event? resultm)
false)}))
(defn- event->params
[event]
(let [params {:id (uuid/next)
:name (::name event)
:type (::type event)
:profile-id (::profile-id event)
:ip-addr (::ip-addr event)
:context (::context event {})
:props (::props event {})
:source "backend"}
tnow (::tracked-at event)]
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context (some-> params meta ::http/request prepare-context-from-request)
context (assoc context :request-id (::rpc/request-id params))
request-at (::rpc/request-at params)]
{:type "action"
:profile-id (::rpc/profile-id params)
:created-at request-at
:tracked-at request-at
:ip-addr (::rpc/ip-addr params)
:context (d/without-nils context)}))
(cond-> params
(some? tnow)
(assoc :tracked-at tnow))))
(defn- append-audit-entry!
[cfg params]
(let [params (-> params
(update :props db/tjson)
(update :context db/tjson)
(update :ip-addr db/inet))]
(db/insert! cfg :audit-log params)))
(defn- handle-event!
(defn submit
"Submit an event to be registered under audit-log subsystem"
[cfg event]
(let [params (event->params event)
tnow (ct/now)]
(let [tnow (ct/now)
event (-> event
(assoc :created-at tnow)
(update :profile-id d/nilv uuid/zero)
(update :tracked-at d/nilv tnow)
(update :ip-addr d/nilv "0.0.0.0")
(update :props d/nilv {})
(update :context d/nilv {})
(assoc :source "backend")
(d/without-nils))]
(submit* cfg event)))
(when (contains? cf/flags :audit-log)
;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in
;; this case we just retry the operation.
(let [params (-> params
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))
(when (and (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
(not (contains? cf/flags :audit-log)))
;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in
;; this case we just retry the operation.
;;
;; NOTE: this is only executed when general audit log is disabled
(let [params (-> params
(assoc :created-at tnow)
(update :tracked-at #(or % tnow))
(assoc :props {})
(assoc :context {}))]
(append-audit-entry! cfg params)))
(when (and (contains? cf/flags :webhooks)
(::webhooks/event? event))
(let [batch-key (::webhooks/batch-key event)
batch-timeout (::webhooks/batch-timeout event)
label (dm/str "rpc:" (:name params))
label (cond
(ifn? batch-key) (dm/str label ":" (batch-key (::rpc/params event)))
(string? batch-key) (dm/str label ":" batch-key)
:else label)
dedupe? (boolean (and batch-key batch-timeout))]
(wrk/submit! (-> cfg
(assoc ::wrk/task :process-webhook-event)
(assoc ::wrk/queue :webhooks)
(assoc ::wrk/max-retries 0)
(assoc ::wrk/delay (or batch-timeout 0))
(assoc ::wrk/dedupe dedupe?)
(assoc ::wrk/label label)
(assoc ::wrk/params (-> params
(dissoc :source)
(dissoc :context)
(dissoc :ip-addr)
(dissoc :type)))))))
params))
(defn submit!
"Submit audit event to the collector."
[cfg event]
(try
(let [event (-> (d/without-nils event)
(check-event))
cfg (-> cfg
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 6)
(assoc ::rtry/label "persist-audit-log"))]
(rtry/invoke! cfg db/tx-run! handle-event! event))
(catch Throwable cause
(l/error :hint "unexpected error processing event" :cause cause))))
(defn insert!
(defn insert
"Submit audit event to the collector, intended to be used only from
command line helpers because this skips all webhooks and telemetry
logic."
[cfg event]
(when (contains? cf/flags :audit-log)
(let [event (-> (d/without-nils event)
(let [tnow (ct/now)
event (-> event
(assoc :created-at tnow)
(update :tracked-at d/nilv tnow)
(update :profile-id d/nilv uuid/zero)
(update :props d/nilv {})
(update :context d/nilv {})
(assoc :source "backend")
(select-keys event-keys)
(check-event))]
(db/run! cfg (fn [cfg]
(let [tnow (ct/now)
params (-> (event->params event)
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))))))
(db/run! cfg append-audit-entry event))))
@@ -10,14 +10,11 @@
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[integrant.core :as ig]
[lambdaisland.uri :as u]
[promesa.exec :as px]))
;; This is a task responsible to send the accumulated events to
@@ -52,19 +49,18 @@
(defn- send!
[{:keys [::uri] :as cfg} events]
(let [token (tokens/generate cfg
{:iss "authentication"
:uid uuid/zero})
(let [skey (-> cfg ::setup/shared-keys :nexus)
body (t/encode {:events events})
headers {"content-type" "application/transit+json"
"origin" (str (cf/get :public-uri))
"cookie" (u/map->query-string {:auth-token token})}
"x-shared-key" (str "nexus " skey)}
params {:uri uri
:timeout 12000
:method :post
:headers headers
:body body}
resp (http/req! cfg params)]
resp (http/req cfg params {:skip-ssrf-check? true})]
(if (= (:status resp) 204)
true
(do
@@ -109,7 +105,7 @@
(def ^:private schema:handler-params
[:map
::db/pool
::setup/props
::setup/shared-keys
::http/client])
(defmethod ig/assert-key ::handler
+168 -65
View File
@@ -14,6 +14,8 @@
[app.common.schema :as sm]
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[promesa.exec :as px]
@@ -28,69 +30,145 @@
(defonce enabled (atom true))
(defn- persist-on-database!
[pool id report]
[pool id version report]
(when-not (db/read-only? pool)
(db/insert! pool :server-error-report
{:id id
:version 3
:version version
:content (db/tjson report)})))
(defn record->report
(defn- concurrent-exception?
[cause]
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause)))
(defn- log-record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record")
(if (or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause))
(-> record
(assoc ::trace (ex/format-throwable cause :data? true :explain? false :header? false :summary? false))
(assoc ::l/cause (ex-cause cause))
(record->report))
(let [data (if (concurrent-exception? cause)
(ex-data (ex-cause cause))
(ex-data cause))
(let [data (ex-data cause)
ctx (-> context
(assoc :tenant (cf/get :tenant))
(assoc :host (cf/get :host))
(assoc :public-uri (str (cf/get :public-uri)))
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]
ctx (-> context
(assoc :service/tenant (cf/get :tenant))
(assoc :service/host (cf/get :host))
(assoc :service/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version))
(assoc :logger/name logger)
(assoc :logger/level level)
(dissoc :request/params :value :params :data))]
(merge
{:context (-> (into (sorted-map) ctx)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (or (when-let [message (ex-message cause)]
(if-let [props-hint (:hint props)]
(str props-hint ": " message)
message))
@message)
:trace (or (::trace record)
(some-> cause (ex/format-throwable :data? true :explain? false :header? false :summary? false)))}
(merge
{:context (-> (into (sorted-map) ctx)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (or (when-let [message (ex-message cause)]
(if-let [props-hint (:hint props)]
(str props-hint ": " message)
message))
@message)
:trace (or (::trace record)
(some-> cause (ex/format-throwable :data? true :explain? false :header? false :summary? false)))}
(when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :length 20 :level 20)})
(when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :length 20 :level 20)})
(when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 13)})
(when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 13)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :length 30 :level 13)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :length 30 :level 13)})
(when-let [explain (ex/explain data :length 30 :level 13)]
{:explain explain})))))
(when-let [explain (ex/explain data :length 30 :level 13)]
{:explain explain}))))
(defn error-record?
[{:keys [::l/level]}]
(= :error level))
(defn- handle-event
(defn- handle-log-record
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try
(let [uri (cf/get :public-uri)
report (-> record record->report d/without-nils)]
(l/debug :hint "registering error on database" :id id
:uri (str uri "/dbg/error/" id))
report (-> record log-record->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id (str id)
:src "logging"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 3 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(persist-on-database! pool id report))
(defn- audit-event->report
[{:keys [context props ip-addr] :as record}]
(let [context
(reduce-kv (fn [context k v]
(let [k' (keyword "frontend" (name k))]
(-> context
(dissoc k)
(assoc k' v))))
context
context)
context
(-> context
(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))
(assoc :frontend/ip-addr ip-addr))]
{:context (-> (into (sorted-map) context)
(pp/pprint-str :length 50))
:origin (:name record)
:href (get props :href)
:hint (get props :hint)
:report (get props :report)}))
(defn- handle-audit-event
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event audit-event->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id (str id)
:src "audit-log"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 4 report))
(catch Throwable 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))))
@@ -100,26 +178,51 @@
(defmethod ig/init-key ::reporter
[_ cfg]
(let [input (sp/chan :buf (sp/sliding-buffer 64)
:xf (filter error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(let [input (sp/chan :buf (sp/sliding-buffer 256))
thread (px/thread
{:name "penpot/reporter/database"}
(l/info :hint "initializing database error persistence")
(try
(loop []
(when-let [item (sp/take! input)]
(cond
(::l/id item)
(handle-log-record cfg item)
(px/thread {:name "penpot/database-reporter"}
(l/info :hint "initializing database error persistence")
(try
(loop []
(when-let [record (sp/take! input)]
(handle-event cfg record)
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "reporter terminated"))))))
(::rlimit/id item)
(handle-rlimit-event cfg item)
(-> item meta ::audit/event)
(handle-audit-event cfg item)
:else
(l/warn :hint "received unexpected item" :item item))
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(l/info :hint "reporter terminated"))))]
(add-watch l/log-record ::reporter
(fn [_ _ _ record]
(when (= :error (::l/level record))
(sp/put! input record))))
{::input input
::thread thread}))
(defmethod ig/halt-key! ::reporter
[_ thread]
(some-> thread px/interrupt!))
[_ {:keys [::input ::thread]}]
(remove-watch l/log-record ::reporter)
(sp/close! input)
(px/interrupt! thread))
(defn emit
"Emit an event/report into the database reporter"
[cfg event]
(when-let [{:keys [::input]} (get cfg ::reporter)]
(sp/put! input event)))
+137 -57
View File
@@ -9,9 +9,12 @@
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.config :as cf]
[app.http.client :as http]
[app.loggers.database :as ldb]
[app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[app.util.json :as json]
[integrant.core :as ig]
[promesa.exec :as px]
@@ -20,58 +23,110 @@
(defonce enabled (atom true))
(defn- send-mattermost-notification!
[cfg {:keys [id public-uri] :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 [text (str "Exception: " public-uri "/dbg/error/" id " "
(when-let [pid (:profile-id report)]
(str "(pid: #uuid-" pid ")"))
(if (uuid? pid)
(str "(pid: #uuid-" pid ")")
(str "(pid: #ip-" pid ")")))
"\n"
"- host: #" (:host report) "\n"
"- tenant: #" (:tenant report) "\n"
"- logger: #" (:logger report) "\n"
"- request-path: `" (:request-path report) "`\n"
"- frontend-version: `" (:frontend-version report) "`\n"
"- backend-version: `" (:backend-version report) "`\n"
"- origin: #" (:origin report) "\n"
(when-let [href (get report :href)]
(str "- href: `" href "`\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"
"Trace:\n"
(:trace report)
"```")
(when-let [info (:info report)]
(str "```\n" info "```"))
(when-let [trace (:trace report)]
(str "```\n"
"Trace:\n"
trace
"```")))
resp (http/req! cfg
{:uri (cf/get :error-report-webhook)
:method :post
:headers {"content-type" "application/json"}
:body (json/encode-str {:text text})}
{:sync? true})]
resp (http/req cfg
{:uri (cf/get :error-report-webhook)
:method :post
:headers {"content-type" "application/json"}
:body (json/encode-str {:text text})}
{:sync? true})]
(when (not= 200 (:status resp))
(l/warn :hint "error on sending data"
:response (pr-str resp)))))
(defn record->report
[{:keys [::l/context ::l/id ::l/cause] :as record}]
(defn- log-record->report
[{:keys [::l/context ::l/id ::l/cause ::l/message] :as record}]
(assert (l/valid-record? record) "expectd valid log record")
(let [public-uri (cf/get :public-uri)]
{:id id
:type "exception"
:origin "logging"
:hint (or (some-> cause ex-message) @message)
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
:frontend-version (:frontend/version context)
:profile-id (:request/profile-id context)
:href (-> public-uri
(assoc :path (:request/path context))
(str))
:trace (ex/format-throwable cause :detail? false :header? false)}))
(defn- audit-event->report
[{:keys [context props id] :as event}]
{:id id
:type "exception"
:origin "audit-log"
:hint (get props :hint)
:tenant (cf/get :tenant)
:host (cf/get :host)
:public-uri (cf/get :public-uri)
:backend-version (or (:version/backend context) (:full cf/version))
:frontend-version (:version/frontend context)
:profile-id (:request/profile-id context)
:request-path (:request/path context)
:logger (::l/logger record)
:trace (ex/format-throwable cause :detail? false :header? false)})
:backend-version (:full cf/version)
:frontend-version (:version context)
:profile-id (:profile-id event)
:href (get props :href)})
(defn handle-event
[cfg record]
(when @enabled
(try
(let [report (record->report record)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause)))))
(defn- rlimit-event->report
[event]
{:id (::rlimit/id event)
:type "notification"
:origin "rlimit"
:hint (str "rlimit reject of "
(::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-event
[cfg event event->report]
(try
(let [report (event->report event)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause))))
(defmethod ig/assert-key ::reporter
[_ params]
@@ -80,27 +135,52 @@
(defmethod ig/init-key ::reporter
[_ cfg]
(when-let [uri (cf/get :error-report-webhook)]
(px/thread
{:name "penpot/mattermost-reporter"
:virtual true}
(l/info :hint "initializing error reporter" :uri uri)
(let [input (sp/chan :buf (sp/sliding-buffer 128)
:xf (filter ldb/error-record?))]
(add-watch l/log-record ::reporter #(sp/put! input %4))
(try
(loop []
(when-let [msg (sp/take! input)]
(handle-event cfg msg)
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "reporter terminated")))))))
(let [input (sp/chan :buf (sp/sliding-buffer 256))
thread (px/thread
{:name "penpot/reporter/mattermost"}
(l/info :hint "initializing error reporter" :uri uri)
(try
(loop []
(when-let [item (sp/take! input)]
(when @enabled
(cond
(::l/id item)
(handle-event cfg item log-record->report)
(::rlimit/id item)
(handle-event cfg item rlimit-event->report)
(-> item meta ::audit/event)
(handle-event cfg item audit-event->report)
:else
(l/warn :hint "received unexpected item" :item item)))
(recur)))
(catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(finally
(l/info :hint "reporter terminated"))))]
(add-watch l/log-record ::reporter
(fn [_ _ _ record]
(when (= :error (::l/level record))
(sp/put! input record))))
{::input input
::thread thread})))
(defmethod ig/halt-key! ::reporter
[_ thread]
[_ {:keys [::input ::thread]}]
(remove-watch l/log-record ::reporter)
(some-> input sp/close!)
(some-> thread px/interrupt!))
(defn emit
"Emit an event/report into the mattermost reporter"
[cfg event]
(when-let [{:keys [::input]} (get cfg ::reporter)]
(sp/put! input event)))
+16 -9
View File
@@ -70,14 +70,14 @@
(fn [{:keys [props] :as task}]
(let [items (lookup-webhooks cfg props)
event {::audit/profile-id (:profile-id props)
::audit/name "webhook"
::audit/type "trigger"
::audit/props {:name (get props :name)
:event-id (get props :id)
:total-affected (count items)}}]
event {:profile-id (:profile-id props)
:name "webhook"
:type "trigger"
:props {:name (get props :name)
:event-id (get props :id)
:total-affected (count items)}}]
(audit/insert! cfg event)
(audit/insert cfg event)
(when items
(l/trc :hint "webhooks found for event" :total (count items))
@@ -159,7 +159,7 @@
:method :post
:body body}]
(try
(let [rsp (http/req! cfg req {:response-type :input-stream :sync? true})
(let [rsp (http/req cfg req {:response-type :input-stream :sync? true})
err (interpret-response rsp)]
(report-delivery! whook req rsp err)
(update-webhook! whook err))
@@ -190,4 +190,11 @@
"invalid-uri"
(instance? java.net.http.HttpConnectTimeoutException cause)
"timeout"))
"timeout"
:else
(let [data (ex-data cause)]
(if (and (= :validation (:type data))
(= :ssrf-blocked-target (:code data)))
(str "blocked-request:" (:hint data))
nil))))
+53 -28
View File
@@ -61,21 +61,15 @@
::mdef/help "A total number of bytes processed by update-file."
::mdef/type :counter}
:rpc-mutation-timing
{::mdef/name "penpot_rpc_mutation_timing"
::mdef/help "RPC mutation method call timing."
:rpc-main-timing
{::mdef/name "penpot_rpc_main_timing"
::mdef/help "RPC command method call timing for main"
::mdef/labels ["name"]
::mdef/type :histogram}
:rpc-command-timing
{::mdef/name "penpot_rpc_command_timing"
::mdef/help "RPC command method call timing."
::mdef/labels ["name"]
::mdef/type :histogram}
:rpc-query-timing
{::mdef/name "penpot_rpc_query_timing"
::mdef/help "RPC query method call timing."
:rpc-management-timing
{::mdef/name "penpot_rpc_management_timing"
::mdef/help "RPC command method call timing for management."
::mdef/labels ["name"]
::mdef/type :histogram}
@@ -226,11 +220,10 @@
::http/server
{::http/port (cf/get :http-server-port)
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::http/router (ig/ref ::http/router)
::mtx/metrics (ig/ref ::mtx/metrics)}
::ldap/provider
@@ -305,10 +298,11 @@
::session/manager (ig/ref ::session/manager)}
:app.http.assets/routes
{::http.assets/path (cf/get :assets-path)
::http.assets/cache-max-age (ct/duration {:hours 24})
::http.assets/cache-max-agesignature-max-age (ct/duration {:hours 24 :minutes 5})
::sto/storage (ig/ref ::sto/storage)}
{::http.assets/path (cf/get :assets-path)
::http.assets/cache-max-age (ct/duration {:hours 24})
::http.assets/signature-max-age (ct/duration {:hours 24 :minutes 15})
::sto/storage (ig/ref ::sto/storage)
::session/manager (ig/ref ::session/manager)}
::rpc/climit
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -317,12 +311,19 @@
::climit/enabled (contains? cf/flags :rpc-climit)}
: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
{::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::rds/pool (ig/ref ::rds/pool)
:app.nitrate/client (ig/ref :app.nitrate/client)
::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider)
@@ -337,7 +338,17 @@
::setup/props (ig/ref ::setup/props)
::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist)}
::email/whitelist (ig/ref ::email/whitelist)
:app.loggers.database/reporter
(ig/ref :app.loggers.database/reporter)
:app.loggers.mattermost/reporter
(ig/ref :app.loggers.mattermost/reporter)}
:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)
::setup/shared-keys (ig/ref ::setup/shared-keys)}
:app.rpc/management-methods
{::http.client/client (ig/ref ::http.client/client)
@@ -348,17 +359,19 @@
::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus)
:app.nitrate/client (ig/ref :app.nitrate/client)
::rds/client (ig/ref ::rds/client)
::setup/props (ig/ref ::setup/props)}
::rpc/routes
{::rpc/methods (ig/ref :app.rpc/methods)
{::rpc/methods (ig/ref :app.rpc/methods)
::rpc/management-methods (ig/ref :app.rpc/management-methods)
;; FIXME: revisit if db/pool is necessary here
::db/pool (ig/ref ::db/pool)
::session/manager (ig/ref ::session/manager)
::setup/props (ig/ref ::setup/props)}
::setup/props (ig/ref ::setup/props)
::setup/shared-keys (ig/ref ::setup/shared-keys)}
::wrk/registry
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -370,6 +383,7 @@
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:upload-session-gc (ig/ref :app.tasks.upload-session-gc/handler)
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
:session-gc (ig/ref ::session.tasks/gc)
@@ -405,6 +419,9 @@
:app.tasks.tasks-gc/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.upload-session-gc/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.objects-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
@@ -446,13 +463,19 @@
;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)}
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nexus (cf/get :nexus-shared-key)
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock
{}
:app.loggers.audit.archive-task/handler
{::setup/props (ig/ref ::setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)}
{::setup/shared-keys (ig/ref ::setup/shared-keys)
::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)}
:app.loggers.audit.gc-task/handler
{::db/pool (ig/ref ::db/pool)}
@@ -520,6 +543,9 @@
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc}
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
:task :upload-session-gc}
{:cron #penpot/cron "0 0 2 * * ?" ;; daily
:task :file-gc-scheduler}
@@ -627,9 +653,8 @@
[& _args]
(try
(let [p (promise)]
(when (contains? cf/flags :nrepl-server)
(l/inf :hint "start nrepl server" :port 6064)
(nrepl/start-server :bind "0.0.0.0" :port 6064))
(l/inf :hint "start nrepl server" :port 6064)
(nrepl/start-server :bind "0.0.0.0" :port 6064)
(start)
(deref p))
+93 -50
View File
@@ -18,6 +18,7 @@
[app.config :as cf]
[app.db :as-alias db]
[app.http.client :as http]
[app.media.sanitize :as sanitize]
[app.storage :as-alias sto]
[app.storage.tmp :as tmp]
[buddy.core.bytes :as bb]
@@ -31,15 +32,11 @@
(:import
clojure.lang.XMLHandler
java.io.InputStream
javax.xml.XMLConstants
javax.xml.parsers.SAXParserFactory
javax.xml.XMLConstants
org.apache.commons.io.IOUtils
org.im4java.core.ConvertCmd
org.im4java.core.IMOperation
org.im4java.core.Info))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
org.im4java.core.IMOperation))
(def schema:upload
[:map {:title "Upload"}
@@ -79,6 +76,20 @@
max-size)))
upload))
(defn validate-font-size!
"Validates that the font file `upload` does not exceed the configured
`:font-max-file-size` limit. Accepts the same map shape as
`validate-media-size!` — requires a `:size` key in bytes."
[upload]
(let [max-size (cf/get :font-max-file-size)]
(when (> (:size upload) max-size)
(ex/raise :type :restriction
:code :font-max-file-size-reached
:hint (str/ffmt "the uploaded font size % is greater than the maximum %"
(:size upload)
max-size)))
upload))
(defmulti process :cmd)
(defmulti process-error class)
@@ -224,17 +235,18 @@
;; If we are processing an animated gif we use the first frame with -scene 0
(let [dim-result (sh/sh "identify" "-format" "%w %h\n" path)
orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)]
(if (and (= 0 (:exit dim-result))
(= 0 (:exit orient-result)))
(when (= 0 (:exit dim-result))
(let [[w h] (-> (:out dim-result)
str/trim
(clojure.string/split #"\s+")
(->> (mapv #(Integer/parseInt %))))
orientation (-> orient-result :out str/trim)]
(case orientation
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
{:width w :height h})) ; Normal or unknown orientation
nil)))
orientation-exit (:exit orient-result)
orientation (-> orient-result :out str/trim)]
(if (= 0 orientation-exit)
(case orientation
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
{:width w :height h}) ; Normal or unknown orientation
{:width w :height h}))))) ; If orientation can't be read, use dimensions as-is
(defmethod process :info
[{:keys [input] :as params}]
@@ -247,26 +259,37 @@
:hint "uploaded svg does not provides dimensions"))
(merge input info {:ts (ct/now) :size (fs/size path)}))
(let [instance (Info. (str path))
mtype' (.getProperty instance "Mime type")]
(let [path-str (str path)
identify-res (sh/sh "identify" "-format" "image/%[magick]\n" path-str)
;; identify prints one line per frame (animated GIFs, etc.); we take the first one
mtype' (if (zero? (:exit identify-res))
(-> identify-res
:out
str/trim
(str/split #"\s+" 2)
first
str/lower)
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image"))
{:keys [width height]}
(or (get-dimensions-with-orientation path-str)
(do
(l/warn "Failed to read image dimensions with orientation" {:path path})
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image")))]
(when (and (string? mtype)
(not= mtype mtype'))
(not= (str/lower mtype) mtype'))
(ex/raise :type :validation
:code :media-type-mismatch
:hint (str "Seems like you are uploading a file whose content does not match the extension."
"Expected: " mtype ". Got: " mtype')))
(let [{:keys [width height]}
(or (get-dimensions-with-orientation (str path))
(do
(l/warn "Failed to read image dimensions with orientation; falling back to im4java"
{:path path})
{:width (.getPageWidth instance)
:height (.getPageHeight instance)}))]
(assoc input
:width width
:height height
:size (fs/size path)
:ts (ct/now)))))))
(assoc input
:width width
:height height
:size (fs/size path)
:ts (ct/now))))))
(defmethod process-error org.im4java.core.InfoException
[error]
@@ -282,34 +305,52 @@
(defn download-image
"Download an image from the provided URI and return the media input object"
[{:keys [::http/client]} uri]
(letfn [(parse-and-validate [{:keys [headers] :as response}]
(letfn [(parse-and-validate [{:keys [status headers] :as response}]
(let [size (some-> (get headers "content-length") d/parse-integer)
mtype (get headers "content-type")
format (cm/mtype->format mtype)
max-size (cf/get :media-max-file-size default-max-file-size)]
mtype (get headers "content-type")]
(when-not (<= 200 status 299)
(ex/raise :type :validation
:code :unable-to-download-image
:hint (str/ffmt "unable to download image from '%': unexpected status code %" uri status)))
(when-not size
(ex/raise :type :validation
:code :unknown-size
:hint "seems like the url points to resource with unknown size"))
(when (> size max-size)
(ex/raise :type :validation
:code :file-too-large
:hint (str/ffmt "the file size % is greater than the maximum %"
size
default-max-file-size)))
(-> {:size size :mtype mtype}
(validate-media-type!)
(validate-media-size!))))]
(when (nil? format)
(ex/raise :type :validation
:code :media-type-not-allowed
:hint "seems like the url points to an invalid media object"))
(let [{:keys [body] :as response}
(try
(http/req-with-redirects
client
{:method :get :uri uri}
{:response-type :input-stream
:max-redirects 3})
(catch java.net.ConnectException cause
(ex/raise :type :validation
:code :unable-to-download-image
:hint (str/ffmt "unable to download image from '%': connection refused or host unreachable" uri)
:cause cause))
(catch java.net.http.HttpConnectTimeoutException cause
(ex/raise :type :validation
:code :unable-to-download-image
:hint (str/ffmt "unable to download image from '%': connection timeout" uri)
:cause cause))
(catch java.net.http.HttpTimeoutException cause
(ex/raise :type :validation
:code :unable-to-download-image
:hint (str/ffmt "unable to download image from '%': request timeout" uri)
:cause cause))
(catch java.io.IOException cause
(ex/raise :type :validation
:code :unable-to-download-image
:hint (str/ffmt "unable to download image from '%': I/O error" uri)
:cause cause)))
{:size size :mtype mtype :format format}))]
(let [{:keys [body] :as response} (http/req! client
{:method :get :uri uri}
{:response-type :input-stream})
{:keys [size mtype]} (parse-and-validate response)
path (tmp/tempfile :prefix "penpot.media.download.")
written (io/write* path body :size size)]
@@ -319,9 +360,11 @@
:code :mismatch-write-size
:hint "unexpected state: unable to write to file"))
{;; :size size
:path path
:mtype mtype})))
;; Sanitize: strip trailing data after image EOF markers
(let [new-size (sanitize/truncate-after-eof path mtype)]
{:path path
:mtype mtype
:size new-size}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FONTS
+191
View File
@@ -0,0 +1,191 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.media.sanitize
"Image EOF truncation helpers — strips trailing data after image EOF
markers to prevent exfiltration of non-image bytes appended to
valid image files."
(:require
[app.common.buffer :as buf]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.util.nio :as nio])
(:import
java.nio.ByteOrder
java.nio.channels.FileChannel))
(set! *warn-on-reflection* true)
(defn- scan-backwards
"Scan byte array `arr` backwards (from the end) for the byte pattern
`marker`. Returns the index in `arr` where the marker starts, or -1
if not found."
[^bytes arr ^bytes marker]
(let [arr-len (alength arr)
marker-len (alength marker)]
(loop [i (- arr-len marker-len)]
(if (< i 0)
-1
(if (loop [j 0]
(if (>= j marker-len)
true
(if (= (aget arr (+ i j)) (aget marker j))
(recur (inc j))
false)))
i
(recur (dec i)))))))
(defn- find-last-png-iend
"Find the byte offset of the end of the PNG IEND chunk (12 bytes:
4-byte length + 4-byte 'IEND' + 4-byte CRC32). Returns the offset
AFTER the CRC32, or nil if not found."
[^FileChannel channel]
(let [size (nio/channel-size channel)]
(when (> size 8)
(let [buf-size (min (int size) (* 1024 1024))
marker (byte-array [0x49 0x45 0x4E 0x44])] ;; "IEND"
(loop [pos (max 0 (- size buf-size))]
(when (< pos size)
(let [arr (nio/read-at channel pos buf-size)
idx (scan-backwards arr marker)]
(if (neg? idx)
;; Not found in this chunk, try earlier
(let [next-pos (max 0 (- pos (- buf-size 4)))]
(when (< next-pos pos)
(recur next-pos)))
;; Found "IEND" at idx. Chunk starts 4 bytes before.
(let [chunk-start (- (+ pos idx) 4)]
(when (>= chunk-start 0)
;; PNG chunk length is big-endian (network byte order).
;; buf/wrap defaults to little-endian, so set it to big-endian.
(let [len-arr (nio/read-at channel chunk-start 4)
len-buf (buf/set-order (buf/wrap len-arr) ByteOrder/BIG_ENDIAN)
chunk-len (buf/read-int len-buf 0)]
(when (zero? chunk-len)
(+ chunk-start 12)))))))))))))
(defn- find-last-jpeg-eoi
"Find the byte offset of the last JPEG EOI marker (0xFF 0xD9).
Returns the offset AFTER the marker, or nil if not found."
[^FileChannel channel]
(let [size (nio/channel-size channel)]
(when (> size 2)
(let [buf-size (min (int size) (* 1024 1024))
marker (byte-array [(unchecked-byte 0xFF) (unchecked-byte 0xD9)])]
(loop [pos (max 0 (- size buf-size))]
(when (< pos size)
(let [arr (nio/read-at channel pos buf-size)
idx (scan-backwards arr marker)]
(if (neg? idx)
(let [next-pos (max 0 (- pos (- buf-size 2)))]
(when (< next-pos pos)
(recur next-pos)))
(+ pos idx 2)))))))))
(defn- find-last-gif-trailer
"Find the byte offset immediately after the last GIF trailer byte (0x3B).
Scans backwards through the file so that appended data after the real
trailer is truncated even when it ends with 0x3B.
Returns the offset AFTER the trailer byte, or nil if 0x3B is not found."
[^FileChannel channel]
(let [size (nio/channel-size channel)]
(when (pos? size)
(let [buf-size (min (int size) (* 1024 1024))
marker (byte-array [(unchecked-byte 0x3B)])]
(loop [pos (max 0 (- size buf-size))]
(when (< pos size)
(let [arr (nio/read-at channel pos buf-size)
idx (scan-backwards arr marker)]
(if (neg? idx)
(let [next-pos (max 0 (- pos (- buf-size 1)))]
(when (< next-pos pos)
(recur next-pos)))
(+ pos idx 1)))))))))
(defn- find-webp-end
"Parse the WebP RIFF header to find the declared file size.
WebP format: 'RIFF' (4 bytes) + uint32 total-size (4 bytes, little-endian)
+ 'WEBP' (4 bytes). The total size is the offset of the end of the file.
Returns nil if the RIFF or WEBP magic bytes are missing."
[^FileChannel channel]
(let [size (nio/channel-size channel)]
(when (>= size 12)
(let [^bytes arr (nio/read-at channel 0 12)
buf (buf/wrap arr)]
;; Check RIFF magic (bytes 0-3) AND WEBP FourCC (bytes 8-11)
(when (and (= (aget arr 0) (byte 0x52)) ;; 'R'
(= (aget arr 1) (byte 0x49)) ;; 'I'
(= (aget arr 2) (byte 0x46)) ;; 'F'
(= (aget arr 3) (byte 0x46)) ;; 'F'
(= (aget arr 8) (byte 0x57)) ;; 'W'
(= (aget arr 9) (byte 0x45)) ;; 'E'
(= (aget arr 10) (byte 0x42)) ;; 'B'
(= (aget arr 11) (byte 0x50))) ;; 'P'
(let [riff-size (bit-and (buf/read-int buf 4) 0xFFFFFFFF)]
;; RIFF size field is the size of the file minus 8 bytes
(+ riff-size 8)))))))
(defn truncate-after-eof
"Given a `java.nio.file.Path` to a freshly-downloaded media file and a
declared MIME type, truncate the file in place to the position of the
format's EOF marker:
- image/png → end of the IEND chunk (12 bytes: 4-byte length + 4-byte type + 4-byte CRC32)
- image/jpeg → 2 bytes after FFD9
- image/gif → immediately after the last GIF trailer byte 0x3B
- image/webp → end of RIFF chunk declared in bytes 4..8
- image/svg+xml → no-op (text format; processed by SAX parser)
- other → no-op (return path unchanged)
Returns the new file size. Raises `:validation/:invalid-image` if no
EOF marker is found within the file."
[^java.nio.file.Path path ^String mtype]
(try
(with-open [channel (nio/open-channel path)]
(let [size (nio/channel-size channel)]
(if (zero? size)
0
(let [needs-eof-marker? (or (= mtype "image/png")
(= mtype "image/jpeg")
(= mtype "image/gif")
(= mtype "image/webp"))
eof-offset
(cond
(= mtype "image/png") (find-last-png-iend channel)
(= mtype "image/jpeg") (find-last-jpeg-eoi channel)
(= mtype "image/gif") (find-last-gif-trailer channel)
(= mtype "image/webp") (find-webp-end channel)
:else nil)]
(cond
;; No EOF marker applicable (SVG or other) — no-op
(nil? eof-offset)
(if needs-eof-marker?
(ex/raise :type :validation
:code :invalid-image
:hint "image format EOF marker not found")
size)
;; Truncate if needed
(< eof-offset size)
(do
(l/dbg :hint "truncating trailing data"
:path (str path)
:mtype mtype
:original-size size
:truncated-to eof-offset)
(nio/truncate channel eof-offset)
eof-offset)
;; Already at correct size or marker at end
:else
eof-offset)))))
(catch Exception e
(if (ex/exception? e)
(throw e)
(ex/raise :type :validation
:code :invalid-image
:hint "failed to sanitize image"
:cause e)))))
+2 -2
View File
@@ -15,16 +15,16 @@
io.prometheus.client.CollectorRegistry
io.prometheus.client.Counter
io.prometheus.client.Counter$Child
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.Gauge
io.prometheus.client.Gauge$Child
io.prometheus.client.Histogram
io.prometheus.client.Histogram$Child
io.prometheus.client.hotspot.DefaultExports
io.prometheus.client.SimpleCollector
io.prometheus.client.Summary
io.prometheus.client.Summary$Builder
io.prometheus.client.Summary$Child
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.hotspot.DefaultExports
java.io.StringWriter))
(set! *warn-on-reflection* true)
+23 -1
View File
@@ -10,6 +10,7 @@
[app.common.logging :as l]
[app.db :as db]
[app.migrations.clj.migration-0023 :as mg0023]
[app.migrations.clj.migration-0145 :as mg0145]
[app.util.migrations :as mg]
[integrant.core :as ig]))
@@ -456,7 +457,28 @@
:fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")}
{:name "0143-http-session-v2-table"
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}])
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
{:name "0144-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
{:name "0145-fix-plugins-uri-on-profile"
:fn mg0145/migrate}
{:name "0145-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0145-mod-audit-log-table.sql")}
{:name "0145-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0145-mod-audit-log-table.sql")}
{:name "0146-mod-audit-log-table"
:fn (mg/resource "app/migrations/sql/0146-mod-audit-log-table.sql")}
{:name "0146-mod-access-token-table"
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}
{:name "0147-add-upload-session-table"
:fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")}])
(defn apply-migrations!
[pool name migrations]
@@ -0,0 +1,83 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.migrations.clj.migration-0145
"Migrate plugins references on profiles"
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[cuerdas.core :as str]))
(def ^:private replacements
{"https://colors-to-tokens-plugin.pages.dev"
"https://colors-to-tokens.plugins.penpot.app"
"https://contrast-penpot-plugin.pages.dev"
"https://contrast.plugins.penpot.app"
"https://create-palette-penpot-plugin.pages.dev"
"https://create-palette.plugins.penpot.app"
"https://icons-penpot-plugin.pages.dev"
"https://icons.plugins.penpot.app"
"https://lorem-ipsum-penpot-plugin.pages.dev"
"https://lorem-ipsum.plugins.penpot.app"
"https://rename-layers-penpot-plugin.pages.dev"
"https://rename-layers.plugins.penpot.app"
"https://table-penpot-plugin.pages.dev"
"https://table.plugins.penpot.app"})
(defn- fix-url
[url]
(reduce-kv (fn [url prefix replacement]
(if (str/starts-with? url prefix)
(reduced (str replacement (subs url (count prefix))))
url))
url
replacements))
(defn- fix-manifest
[manifest]
(-> manifest
(d/update-when :url fix-url)
(d/update-when :host fix-url)))
(defn- fix-plugins-data
[props]
(d/update-in-when props [:plugins :data]
(fn [data]
(reduce-kv (fn [data id manifest]
(let [manifest' (fix-manifest manifest)]
(if (= manifest manifest')
data
(assoc data id manifest'))))
data
data))))
(def ^:private sql:get-profiles
"SELECT id, props FROM profile
WHERE props ?? '~:plugins'
ORDER BY created_at
FOR UPDATE")
(defn migrate
[conn]
(->> (db/plan conn [sql:get-profiles])
(run! (fn [{:keys [id props]}]
(when-let [props (some-> props db/decode-transit-pgobject)]
(let [props' (fix-plugins-data props)]
(when (not= props props')
(l/inf :hint "fixing plugins data on profile props" :profile-id (str id))
(db/update! conn :profile
{:props (db/tjson props')}
{:id id}
{::db/return-keys false}))))))))
@@ -0,0 +1,11 @@
ALTER TABLE server_error_report DROP CONSTRAINT server_error_report_pkey;
DELETE FROM server_error_report a
USING server_error_report b
WHERE a.id = b.id
AND a.ctid < b.ctid;
ALTER TABLE server_error_report ADD PRIMARY KEY (id);
CREATE INDEX server_error_report__version__idx
ON server_error_report ( version );
@@ -0,0 +1,2 @@
CREATE INDEX audit_log__created_at__idx ON audit_log(created_at) WHERE archived_at IS NULL;
CREATE INDEX audit_log__archived_at__idx ON audit_log(archived_at) WHERE archived_at IS NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE access_token
ADD COLUMN type text NULL;
@@ -0,0 +1,5 @@
-- Add index on audit_log (source, created_at) to support efficient
-- queries for the telemetry batch collection mode.
CREATE INDEX IF NOT EXISTS audit_log__source__created_at__idx
ON audit_log (source, created_at ASC);
@@ -0,0 +1,14 @@
CREATE TABLE upload_session (
id uuid PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
total_chunks integer NOT NULL
);
CREATE INDEX upload_session__profile_id__idx
ON upload_session(profile_id);
CREATE INDEX upload_session__created_at__idx
ON upload_session(created_at);
+130
View File
@@ -0,0 +1,130 @@
(ns app.nitrate
"Module that make calls to the external nitrate aplication"
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.http.client :as http]
[app.rpc :as-alias rpc]
[app.setup :as-alias setup]
[app.util.json :as json]
[clojure.core :as c]
[integrant.core :as ig]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- request-builder
[cfg method uri shared-key profile-id]
(fn []
(http/req cfg {:method method
:headers {"content-type" "application/json"
"accept" "application/json"
"x-shared-key" shared-key
"x-profile-id" (str profile-id)}
:uri uri
:version :http1.1})))
(defn- with-retries
[handler max-retries]
(fn []
(loop [attempt 1]
(let [result (try
(handler)
(catch Exception e
(if (< attempt max-retries)
::retry
(do
;; TODO Error handling
(l/error :hint "request fail after multiple retries" :cause e)
nil))))]
(if (= result ::retry)
(recur (inc attempt))
result)))))
(defn- with-validate [handler uri schema]
(fn []
(let [coercer-http (sm/coercer schema
:type :validation
:hint (str "invalid data received calling " uri))]
(try
(coercer-http (-> (handler) :body json/decode))
(catch Exception e
;; TODO Error handling
(l/error :hint "error validating json response" :cause e)
nil)))))
(defn- request-to-nitrate
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
(with-retries 3)
(with-validate uri schema))]
(full-http-call)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(let [client (get cfg ::client)
method (get client method)]
(method params))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:organization
[:map
[:id ::sm/text]
[:name ::sm/text]])
(def ^:private schema:user
[:map
[:valid ::sm/boolean]])
(defn- get-team-org
[cfg {:keys [team-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params)))
(defn- is-valid-user
[cfg {:keys [profile-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::client
[_ cfg]
(when (contains? cf/flags :nitrate)
{:get-team-org (partial get-team-org cfg)
:is-valid-user (partial is-valid-user cfg)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UTILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-nitrate-licence-to-profile
[cfg profile]
(try
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
(assoc profile :nitrate-licence (:valid nitrate-licence)))
(catch Throwable cause
(l/error :hint "failed to get nitrate licence"
:profile-id (:id profile)
:cause cause)
profile)))
(defn add-org-to-team
[cfg team params]
(let [params (assoc (or params {}) :team-id (:id team))
org (call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org))))
+10 -10
View File
@@ -24,28 +24,28 @@
[integrant.core :as ig])
(:import
clojure.lang.MapEntry
io.lettuce.core.KeyValue
io.lettuce.core.RedisClient
io.lettuce.core.RedisCommandInterruptedException
io.lettuce.core.RedisCommandTimeoutException
io.lettuce.core.RedisException
io.lettuce.core.RedisURI
io.lettuce.core.ScriptOutputType
io.lettuce.core.SetArgs
io.lettuce.core.api.StatefulRedisConnection
io.lettuce.core.api.sync.RedisCommands
io.lettuce.core.api.sync.RedisScriptingCommands
io.lettuce.core.codec.RedisCodec
io.lettuce.core.codec.StringCodec
io.lettuce.core.KeyValue
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.pubsub.RedisPubSubListener
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
io.lettuce.core.RedisClient
io.lettuce.core.RedisCommandInterruptedException
io.lettuce.core.RedisCommandTimeoutException
io.lettuce.core.RedisException
io.lettuce.core.RedisURI
io.lettuce.core.resource.ClientResources
io.lettuce.core.resource.DefaultClientResources
io.lettuce.core.ScriptOutputType
io.lettuce.core.SetArgs
io.netty.channel.nio.NioEventLoopGroup
io.netty.util.concurrent.EventExecutorGroup
io.netty.util.HashedWheelTimer
io.netty.util.Timer
io.netty.util.concurrent.EventExecutorGroup
java.lang.AutoCloseable
java.time.Duration))
+32 -24
View File
@@ -14,6 +14,7 @@
[app.common.spec :as us]
[app.common.time :as ct]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
@@ -89,16 +90,23 @@
[methods]
(let [methods (update-vals methods peek)]
(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")
session-id (yreq/get-header request "x-session-id")
key-id (get request ::http/auth-key-id)
profile-id (or (::session/profile-id request)
(::actoken/profile-id request))
(::actoken/profile-id request)
(if key-id uuid/zero nil))
ip-addr (inet/parse-request request)
data (-> params
(assoc ::handler-name handler-name)
(assoc ::ip-addr ip-addr)
(assoc ::request-at (ct/now))
(assoc ::request-id (uuid/next))
(assoc ::session-id (some-> session-id uuid/parse*))
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
(assoc ::profile-id profile-id)))
@@ -154,12 +162,13 @@
(defn- wrap-audit
[_ f mdata]
(if (or (contains? cf/flags :webhooks)
(contains? cf/flags :audit-log))
(contains? cf/flags :audit-log)
(contains? cf/flags :telemetry))
(if-not (::audit/skip mdata)
(fn [cfg params]
(let [result (f cfg params)]
(->> (audit/prepare-event cfg mdata params result)
(audit/submit! cfg))
(->> (audit/prepare-rpc-event cfg mdata params result)
(audit/submit cfg))
result))
f)
f))
@@ -222,8 +231,8 @@
(wrap-authentication cfg $ mdata)))
(defn- process-method
[cfg module wrap-fn [f mdata]]
(l/trc :hint "add method" :module module :name (::sv/name mdata))
[cfg wrap-fn [f mdata]]
(l/trc :hint "add method" :module (::module cfg) :type (::type cfg) :name (::sv/name mdata))
(let [f (wrap-fn cfg f mdata)
k (keyword (::sv/name mdata))]
[k [mdata (partial f cfg)]]))
@@ -234,7 +243,7 @@
(defn- resolve-methods
[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
'app.rpc.commands.access-token
'app.rpc.commands.audit
@@ -261,7 +270,7 @@
'app.rpc.commands.verify-token
'app.rpc.commands.viewer
'app.rpc.commands.webhooks)
(map (partial process-method cfg "rpc" wrap))
(map (partial process-method cfg wrap))
(into {}))))
(def ^:private schema:methods-params
@@ -293,11 +302,13 @@
(defn- resolve-management-methods
[cfg]
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
(->> (sv/scan-ns
'app.rpc.management.subscription
'app.rpc.management.exporter)
(map (partial process-method cfg "management" wrap-management))
(let [cfg (assoc cfg ::module "management" ::type "command" ::metrics-id :rpc-management-timing)
mods (cond->> (list 'app.rpc.management.exporter)
(contains? cf/flags :nitrate)
(cons 'app.rpc.management.nitrate))]
(->> (apply sv/scan-ns mods)
(map (partial process-method cfg wrap-management))
(into {}))))
(def ^:private schema:management-methods-params
@@ -340,23 +351,20 @@
(defmethod ig/assert-key ::routes
[_ params]
(assert (map? (::setup/shared-keys params)))
(assert (db/pool? (::db/pool params)) "expect valid database pool")
(assert (some? (::setup/props params)))
(assert (session/manager? (::session/manager params)) "expect valid session manager")
(assert (valid-methods? (::methods params)) "expect valid methods map")
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
(defmethod ig/init-key ::routes
[_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
(let [public-uri (cf/get :public-uri)
management-key (or (cf/get :management-api-key)
(get props :management-key))]
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}]
(let [public-uri (cf/get :public-uri)]
["/api"
["/management"
["/methods/:type"
{:middleware [[mw/shared-key-auth management-key]
["/methods/:method-name"
{:middleware [[mw/shared-key-auth shared-keys]
[session/authz cfg]]
:handler (make-rpc-handler management-methods)}]
@@ -366,7 +374,7 @@
:description "MANAGEMENT API")]
["/main"
["/methods/:type"
["/methods/:method-name"
{:middleware [[mw/cors]
[sec/client-header-check]
[session/authz cfg]
@@ -384,7 +392,7 @@
["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}]
["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}]
["/rpc/command/:type"
["/rpc/command/:method-name"
{:middleware [[mw/cors]
[sec/client-header-check]
[session/authz cfg]
+24 -5
View File
@@ -23,7 +23,7 @@
(dissoc row :perms))
(defn create-access-token
[{:keys [::db/conn] :as cfg} profile-id name expiration]
[{:keys [::db/conn] :as cfg} profile-id name expiration type]
(let [token-id (uuid/next)
expires-at (some-> expiration (ct/in-future))
created-at (ct/now)
@@ -36,6 +36,7 @@
{:id token-id
:name name
:token token
:type type
:profile-id profile-id
:created-at created-at
:updated-at created-at
@@ -50,17 +51,18 @@
(def ^:private schema:create-access-token
[:map {:title "create-access-token"}
[:name [:string {:max 250 :min 1}]]
[:expiration {:optional true} ::ct/duration]])
[:expiration {:optional true} ::ct/duration]
[:type {:optional true} :string]])
(sv/defmethod ::create-access-token
{::doc/added "1.18"
::sm/params schema:create-access-token}
[cfg {:keys [::rpc/profile-id name expiration]}]
[cfg {:keys [::rpc/profile-id name expiration type]}]
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(db/tx-run! cfg create-access-token profile-id name expiration))
(db/tx-run! cfg create-access-token profile-id name expiration type))
(def ^:private schema:delete-access-token
[:map {:title "delete-access-token"}
@@ -83,5 +85,22 @@
(->> (db/query pool :access-token
{:profile-id profile-id}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:id :name :perms :created-at :updated-at :expires-at]})
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
(mapv decode-row)))
(def ^:private schema:get-current-mcp-token
[:map {:title "get-current-mcp-token"}])
(sv/defmethod ::get-current-mcp-token
{::doc/added "2.15"
::sm/params schema:get-current-mcp-token}
[{:keys [::db/pool]} {:keys [::rpc/profile-id ::rpc/request-at]}]
(->> (db/query pool :access-token
{:profile-id profile-id
:type "mcp"}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:token :expires-at]})
(remove #(and (some? (:expires-at %))
(ct/is-after? request-at (:expires-at %))))
(map decode-row)
(first)))
+102 -37
View File
@@ -15,13 +15,16 @@
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
[app.loggers.audit :as-alias audit]
[app.loggers.audit :as audit]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.inet :as inet]
[app.util.services :as sv]))
[app.util.services :as sv]
[clojure.set :as set]))
(def ^:private event-columns
[:id
@@ -36,11 +39,11 @@
:context])
(defn- event->row [event]
[(uuid/next)
[(:id event)
(:name event)
(:source event)
(:type event)
(:timestamp event)
(:tracked-at event)
(:created-at event)
(:profile-id event)
(db/inet (:ip-addr event))
@@ -48,40 +51,92 @@
(db/tjson (d/without-nils (:context event)))])
(defn- adjust-timestamp
[{:keys [timestamp created-at] :as event}]
(let [margin (inst-ms (ct/diff timestamp created-at))]
[{:keys [tracked-at created-at] :as event}]
(let [margin (inst-ms (ct/diff tracked-at created-at))]
(if (or (neg? margin)
(> margin 3600000))
;; If event is in future or lags more than 1 hour, we reasign
;; timestamp to the server creation date
;; tracked-at to the server creation date
(-> event
(assoc :timestamp created-at)
(update :context assoc :original-timestamp timestamp))
(assoc :tracked-at created-at)
(update :context assoc :original-tracked-at tracked-at))
event)))
(defn- handle-events
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(defn- exception-event?
[{:keys [type name] :as ev}]
(and (= "action" type)
(or (= "unhandled-exception" name)
(= "exception-page" name))))
(def ^:private xf:map-event-row
(comp
(map adjust-timestamp)
(map event->row)))
(defn- prepare-events
[{:keys [::rpc/request-at ::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (inet/parse-request request)
tnow (ct/now)
xform (comp
(map (fn [event]
(-> event
(assoc :created-at tnow)
(assoc :profile-id profile-id)
(assoc :ip-addr ip-addr)
(assoc :source "frontend"))))
(filter :profile-id)
(map adjust-timestamp)
(map event->row))
events (sequence xform events)]
(when (seq events)
(db/insert-many! pool :audit-log event-columns events))))
{:id (uuid/next)
:type (:type event)
:name (:name event)
:props (:props event)
:context (:context event)
:profile-id profile-id
:ip-addr ip-addr
:source "frontend"
:tracked-at (:timestamp event)
:created-at request-at}))
(map (fn [item]
(with-meta item {::audit/event true}))))]
(def valid-event-types
#{"action" "identify"})
(sequence xform events)))
(def schema:event
(def ^:private xf:map-telemetry-event-row
(comp
(map adjust-timestamp)
(map (fn [event]
(-> event
(assoc :id (uuid/next))
(update :created-at ct/truncate :days)
(update :tracked-at ct/truncate :days)
(audit/filter-telemetry-props)
(audit/filter-telemetry-context)
(assoc :ip-addr "0.0.0.0")
(assoc :source "telemetry:frontend"))))
(map event->row)))
(defn- handle-events
[{:keys [::db/pool] :as cfg} params]
(let [events (prepare-events params)]
;; Look for error reports and save them on internal reports table
(when-let [events (->> events
(sequence (filter exception-event?))
(not-empty))]
(run! (partial loggers.db/emit cfg) events)
(run! (partial loggers.mm/emit cfg) events))
(when (contains? cf/flags :audit-log)
;; Process and save full audit events when audit-log flag is active
(when-let [rows (-> (sequence xf:map-event-row events)
(not-empty))]
(db/insert-many! pool :audit-log event-columns rows)))
(when (contains? cf/flags :telemetry)
;; Store anonymized frontend events so the telemetry task can ship them
;; in batches. Runs independently from the audit-log insert above so
;; both modes can be active simultaneously.
(when-let [rows (-> (sequence xf:map-telemetry-event-row events)
(not-empty))]
(db/insert-many! pool :audit-log event-columns rows)))))
(def ^:private valid-event-types
#{"action" "identify" "trigger"})
(def ^:private schema:frontend-event
[:map {:title "Event"}
[:name
[:and {:gen/elements ["update-file", "get-profile"]}
@@ -93,12 +148,13 @@
[::sm/one-of {:format "string"} valid-event-types]]]
[:props
[:map-of :keyword ::sm/any]]
[:timestamp ::ct/inst]
[:context {:optional true}
[:map-of :keyword ::sm/any]]])
(def schema:push-audit-events
(def ^:private schema:push-audit-events
[:map {:title "push-audit-events"}
[:events [:vector schema:event]]])
[:events [:vector schema:frontend-event]]])
(sv/defmethod ::push-audit-events
{::climit/id :submit-audit-events/by-profile
@@ -108,17 +164,26 @@
::doc/skip true
::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} params]
(if (or (db/read-only? pool)
(not (contains? cf/flags :audit-log)))
(do
(l/warn :hint "audit: http handler disabled or db is read-only")
(rph/wrap nil))
(do
(let [telemetry? (contains? cf/flags :telemetry)
audit-log? (contains? cf/flags :audit-log)
enabled? (and (not (db/read-only? pool))
(or audit-log? telemetry?))]
(when enabled?
(try
(handle-events cfg params)
(catch Throwable cause
(l/error :hint "unexpected error on persisting audit events from frontend"
:cause cause)))
:cause cause))))
(rph/wrap nil))))
(rph/wrap nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GET-ENABLED-FLAGS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sv/defmethod ::get-enabled-flags
{::audit/skip true
::doc/skip true
::doc/added "1.20"}
[_cfg _params]
(set/intersection cf/flags #{:audit-log :telemetry}))
+143 -93
View File
@@ -253,27 +253,49 @@
:hint "email has complaint reports")))
(defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [fullname email accept-newsletter-updates] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [fullname email] :as params}]
(validate-register-attempt! cfg params)
(let [email (profile/clean-email email)
profile (profile/get-profile-by-email pool email)
params {:email email
:fullname fullname
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:profile-id (:id profile)
:exp (ct/in-future {:days 7})
:props {:newsletter-updates (or accept-newsletter-updates false)}}
profile (profile/get-profile-by-email pool email)]
params (d/without-nils params)
token (tokens/generate cfg params)]
;; SECURITY: refuse to issue a prepared-register token when an active
;; profile already exists for this email.
;;
;; Active accounts must use the standard login flow; existing-but-
;; not-yet-active profiles fall through to the duplicate-detection branch in
;; `register-profile`, which never creates a session.
(when (and (some? profile)
(true? (:is-active profile)))
(ex/raise :type :validation
:code :email-already-exists
:hint "email already exists"))
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
(let [props (-> (audit/extract-utm-params params)
(cond-> (:accept-newsletter-updates params)
(assoc :newsletter-updates true)))
;; SECURITY: do NOT embed `:profile-id` of an existing
;; profile into the prepared-register JWE. Doing so would
;; let an anonymous caller, in possession of a valid
;; team-invitation JWE, ask `register-profile` to load that
;; profile by id and mint a session for it without password
;; verification. `register-profile` independently re-detects
;; duplicates by email and handles them in the
;; "repeated-registry" branch.
params {:email email
:fullname fullname
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:exp (ct/in-future {:days 7})
:props props}
params (d/without-nils params)
token (tokens/generate cfg params)]
(-> {:token token}
(with-meta {::audit/profile-id uuid/zero})))))
(def schema:prepare-register-profile
[:map {:title "prepare-register-profile"}
@@ -281,6 +303,7 @@
[:email ::sm/email]
[:password schema:password]
[:create-welcome-file {:optional true} :boolean]
[:accept-newsletter-updates {:optional true} :boolean]
[:invitation-token {:optional true} schema:token]])
(sv/defmethod ::prepare-register-profile
@@ -307,7 +330,8 @@
:content-type (:mtype input)})]
(:id sobject))
(catch Throwable cause
(l/err :hint "unable to import profile picture"
(l/wrn :hint "unable to import profile picture"
:uri uri
:cause cause)
nil)))
@@ -316,8 +340,7 @@
attrs (all the other attrs are filled with default values)."
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
props (-> (:props params)
(merge {:viewed-tutorial? false
:viewed-walkthrough? false
:nudge {:big 10 :small 1}
@@ -368,7 +391,6 @@
:cause cause)
(throw cause))))))
(defn create-profile-rels
[conn {:keys [id] :as profile}]
(let [features (cfeat/get-enabled-features cf/flags)
@@ -385,48 +407,50 @@
(profile/decode-row))))
(defn send-email-verification!
[{:keys [::db/conn] :as cfg} profile]
(let [vtoken (tokens/generate cfg
{:iss :verify-email
:exp (ct/in-future "72h")
:profile-id (:id profile)
:email (:email profile)})
;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook
ptoken (tokens/generate cfg
{:iss :profile-identity
:profile-id (:id profile)
:exp (ct/in-future {:days 30})})]
(eml/send! {::eml/conn conn
::eml/factory eml/register
:public-uri (cf/get :public-uri)
:to (:email profile)
:name (:fullname profile)
:token vtoken
:extra-data ptoken})))
([cfg profile] (send-email-verification! cfg profile nil))
([{:keys [::db/conn] :as cfg} profile invitation-token]
(let [vclaims (cond-> {:iss :verify-email
:exp (ct/in-future "72h")
:profile-id (:id profile)
:email (:email profile)}
;; If the user registered through a team-invitation flow but
;; their profile is not yet active, we carry the invitation
;; token inside the verify-email JWE so the team-invitation
;; flow can resume after the user clicks the email link.
(some? invitation-token)
(assoc :invitation-token invitation-token))
vtoken (tokens/generate cfg vclaims)
;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook
ptoken (tokens/generate cfg
{:iss :profile-identity
:profile-id (:id profile)
:exp (ct/in-future {:days 30})})]
(eml/send! {::eml/conn conn
::eml/factory eml/register
:public-uri (cf/get :public-uri)
:to (:email profile)
:name (:fullname profile)
:token vtoken
:extra-data ptoken}))))
(defn register-profile
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify cfg {:token token :iss :prepared-register})
params (into claims params)
params (cond-> claims
(:accept-newsletter-updates params)
(update :props assoc :newsletter-updates true))
profile (if-let [profile-id (:profile-id claims)]
(profile/get-profile conn profile-id)
;; NOTE: we first try to match existing profile
;; by email, that in normal circumstances will
;; not return anything, but when a user tries to
;; reuse the same token multiple times, we need
;; to detect if the profile is already registered
(or (profile/get-profile-by-email conn (:email claims))
(let [is-active (or (boolean (:is-active claims))
(boolean (:email-verified claims))
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
(update :password auth/derive-password))
profile (->> (create-profile cfg params)
(create-profile-rels conn))]
(vary-meta profile assoc :created true))))
profile (or (profile/get-profile-by-email conn (:email claims))
(let [is-active (or (boolean (:is-active claims))
(boolean (:email-verified claims))
(not (contains? cf/flags :email-verification)))
params (-> params
(assoc :is-active is-active)
(update :password auth/derive-password))
profile (->> (create-profile cfg params)
(create-profile-rels cfg))]
(vary-meta profile assoc :created true)))
created? (-> profile meta :created true?)
@@ -442,6 +466,7 @@
(when (:create-welcome-file params)
(let [cfg (dissoc cfg ::db/conn)]
(wrk/submit! executor (create-welcome-file cfg profile)))))]
(cond
;; When profile is blocked, we just ignore it and return plain data
(:is-blocked profile)
@@ -449,51 +474,74 @@
(l/wrn :hint "register attempt for already blocked profile"
:profile-id (str (:id profile))
:profile-email (:email profile))
(rph/with-meta {:email (:email profile)}
(rph/with-meta {:id (:id profile)
:email (:email profile)}
{::audit/replace-props props
::audit/context {:action "ignore-because-blocked"}
::audit/profile-id (:id profile)
::audit/name "register-profile-retry"}))
;; If invitation token comes in params, this is because the user
;; comes from team-invitation process; in this case, regenerate
;; token and send back to the user a new invitation token (and
;; mark current session as logged). This happens only if the
;; invitation email matches with the register email.
(and (some? invitation)
(= (:email profile)
(:member-email invitation)))
(let [invitation (assoc invitation :member-id (:id profile))
token (tokens/generate cfg invitation)]
(-> {:invitation-token token}
(rph/with-transform (session/create-fn cfg profile claims))
(rph/with-meta {::audit/replace-props props
::audit/context {:action "accept-invitation"}
::audit/profile-id (:id profile)})))
;; When a new user is created and it is already activated by
;; configuration or specified by OIDC, we just mark the profile
;; as logged-in
;; A profile was just created in this call. Invitation handling is a
;; sub-case of "newly created profile": we never honor invitations for
;; pre-existing profiles via this anonymous RPC. The split below mirrors
;; the non-invitation branches but threads the invitation through the
;; appropriate path:
;;
;; - active + matching invitation → mint session and
;; return :invitation-token. The frontend redirects to
;; :auth-verify-token, which immediately accepts the
;; invitation.
;; - active + no/mismatched invitation → mint session
;; ("login" action). New profile, no further action.
;; - not-active + matching invitation → send the
;; verify-email mail with the invitation token EMBEDDED
;; into the verify-email JWE. No session yet. When the
;; user clicks the link, verify-token activates the
;; profile, mints a session, and propagates the
;; invitation token to the frontend so it can complete
;; the team-invitation flow.
;; - not-active + no/mismatched invitation → standard
;; "check your email" verification flow.
created?
(if (:is-active profile)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg profile claims))
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "login"}
::audit/profile-id (:id profile)}))
(let [accept-invitation? (and (some? invitation)
(= (:email profile)
(:member-email invitation)))]
(cond
(and (:is-active profile) accept-invitation?)
(let [invitation (assoc invitation :member-id (:id profile))
token (tokens/generate cfg invitation)]
(-> {:id (:id profile)
:email (:email profile)
:invitation-token token}
(rph/with-transform (session/create-fn cfg profile claims))
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta {::audit/replace-props props
::audit/context {:action "accept-invitation"}
::audit/profile-id (:id profile)})))
(do
(when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile))
(-> {:email (:email profile)}
(:is-active profile)
(-> (profile/strip-private-attrs profile)
(rph/with-transform (session/create-fn cfg profile claims))
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)}))))
::audit/context {:action "login"}
::audit/profile-id (:id profile)}))
:else
(do
(when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile
(when accept-invitation?
(:invitation-token params))))
(-> {:id (:id profile)
:email (:email profile)}
(rph/with-defer create-welcome-file-when-needed)
(rph/with-meta
{::audit/replace-props props
::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)})))))
:else
(let [elapsed? (elapsed-verify-threshold? profile)
@@ -515,7 +563,8 @@
{:id (:id profile)})
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)}
(rph/with-meta {:email (:email profile)
:id (:id profile)}
{::audit/replace-props (audit/profile->props profile)
::audit/context {:action action}
::audit/profile-id (:id profile)
@@ -523,7 +572,8 @@
(def schema:register-profile
[:map {:title "register-profile"}
[:token schema:token]])
[:token schema:token]
[:accept-newsletter-updates {:optional true} :boolean]])
(sv/defmethod ::register-profile
{::rpc/auth false
+63 -32
View File
@@ -22,6 +22,7 @@
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.media :as media-cmd]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
@@ -80,20 +81,33 @@
;; --- Command: import-binfile
(defn- import-binfile
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
(let [team (teams/get-team pool
:profile-id profile-id
:project-id project-id)
cfg (-> cfg
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/name name)
(assoc ::bfc/input (:path file)))
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file upload-id]}]
(let [team
(teams/get-team pool
:profile-id profile-id
:project-id project-id)
result (case (int version)
1 (bf.v1/import-files! cfg)
3 (bf.v3/import-files! cfg))]
cfg
(-> cfg
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
(assoc ::bfc/project-id project-id)
(assoc ::bfc/profile-id profile-id)
(assoc ::bfc/name name))
input-path (:path file)
owned? (some? upload-id)
cfg
(assoc cfg ::bfc/input input-path)
result
(try
(case (int version)
1 (bf.v1/import-files! cfg)
3 (bf.v3/import-files! cfg))
(finally
(when owned?
(fs/delete input-path))))]
(db/update! pool :project
{:modified-at (ct/now)}
@@ -103,13 +117,18 @@
result))
(def ^:private schema:import-binfile
[:map {:title "import-binfile"}
[:name [:or [:string {:max 250}]
[:map-of ::sm/uuid [:string {:max 250}]]]]
[:project-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:version {:optional true} ::sm/int]
[:file media/schema:upload]])
[:and
[:map {:title "import-binfile"}
[:name [:or [:string {:max 250}]
[:map-of ::sm/uuid [:string {:max 250}]]]]
[:project-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:version {:optional true} ::sm/int]
[:file {:optional true} media/schema:upload]
[:upload-id {:optional true} ::sm/uuid]]
[:fn {:error/message "one of :file or :upload-id is required"}
(fn [{:keys [file upload-id]}]
(or (some? file) (some? upload-id)))]])
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format. If `file-id` is provided,
@@ -117,28 +136,40 @@
The in-place imports are only supported for binfile-v3 and when a
.penpot file only contains one penpot file.
The file content may be provided either as a multipart `file` upload
or as an `upload-id` referencing a completed chunked-upload session,
which allows importing files larger than the multipart size limit.
"
{::doc/added "1.15"
::doc/changes ["1.20" "Add file-id param for in-place import"
"1.20" "Set default version to 3"]
"1.20" "Set default version to 3"
"2.15" "Add upload-id param for chunked upload support"]
::webhooks/event? true
::sse/stream? true
::sm/params schema:import-binfile}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version file-id file] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version file-id upload-id] :as params}]
(projects/check-edition-permissions! pool profile-id project-id)
(let [version (or version 3)
params (-> params
(assoc :profile-id profile-id)
(assoc :version version))
(let [version (or version 3)
params (-> params
(assoc :profile-id profile-id)
(assoc :version version))
cfg (cond-> cfg
(uuid? file-id)
(assoc ::bfc/file-id file-id))
cfg (cond-> cfg
(uuid? file-id)
(assoc ::bfc/file-id file-id))
manifest (case (int version)
1 nil
3 (bf.v3/get-manifest (:path file)))]
params
(if (some? upload-id)
(let [file (db/tx-run! cfg media-cmd/assemble-chunks upload-id)]
(assoc params :file file))
params)
manifest
(case (int version)
1 nil
3 (bf.v3/get-manifest (-> params :file :path)))]
(with-meta
(sse/response (partial import-binfile cfg params))
+74 -136
View File
@@ -79,85 +79,14 @@
;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
and f.deleted_at is null
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
and f.deleted_at is null
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?
and f.deleted_at is null")
(defn get-file-permissions
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(def has-edit-permissions?
(perms/make-edition-predicate-fn get-permissions))
(perms/make-edition-predicate-fn bfc/get-file-permissions))
(def has-read-permissions?
(perms/make-read-predicate-fn get-permissions))
(perms/make-read-predicate-fn bfc/get-file-permissions))
(def has-comment-permissions?
(perms/make-comment-predicate-fn get-permissions))
(perms/make-comment-predicate-fn bfc/get-file-permissions))
(def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?))
@@ -170,7 +99,7 @@
(defn check-comment-permissions!
[conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id share-id)
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
can-read (has-read-permissions? perms)
can-comment (has-comment-permissions? perms)]
(when-not (or can-read can-comment)
@@ -222,7 +151,7 @@
(defn- get-minimal-file-with-perms
[cfg {:keys [:id ::rpc/profile-id]}]
(let [mfile (get-minimal-file cfg id)
perms (get-permissions cfg profile-id id)]
perms (bfc/get-file-permissions cfg profile-id id)]
(assoc mfile :permissions perms)))
(defn get-file-etag
@@ -248,7 +177,7 @@
;; will be already prefetched and we just reuse them instead
;; of making an additional database queries.
(let [perms (or (:permissions (::cond/object params))
(get-permissions conn profile-id id))]
(bfc/get-file-permissions conn profile-id id))]
(check-read-permissions! perms)
(let [team (teams/get-team conn
@@ -311,7 +240,7 @@
::sm/result schema:file-fragment}
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg]
(let [perms (get-permissions cfg profile-id file-id share-id)]
(let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)]
(check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration))))))
@@ -456,8 +385,7 @@
:code :params-validation
:hint "page-id is required when object-id is provided"))
(let [perms (get-permissions conn profile-id file-id share-id)
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id)
file (bfc/get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)})
@@ -688,11 +616,10 @@
"Get libraries used by the specified file."
{::doc/added "1.17"
::sm/params schema:get-file-libraries}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(dm/with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id)
(bfc/get-file-libraries conn file-id)))
[cfg {:keys [::rpc/profile-id file-id]}]
(bfc/check-file-exists cfg file-id)
(check-read-permissions! cfg profile-id file-id)
(bfc/get-file-libraries cfg file-id))
;; --- COMMAND QUERY: Files that use this File library
@@ -777,7 +704,6 @@
f.created_at,
f.modified_at,
f.name,
f.is_shared,
f.deleted_at AS will_be_deleted_at,
ft.media_id AS thumbnail_id,
row_number() OVER w AS row_num,
@@ -785,8 +711,7 @@
FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
AND ft.revn = f.revn
AND ft.deleted_at is null)
AND ft.revn = f.revn)
WHERE p.team_id = ?
AND (p.deleted_at > ?::timestamptz OR
f.deleted_at > ?::timestamptz)
@@ -888,7 +813,7 @@
AND (f.deleted_at IS NULL OR f.deleted_at > now())
ORDER BY f.created_at ASC;")
(defn- absorb-library-by-file!
(defn- absorb-library-by-file
[cfg ldata file-id]
(assert (db/connection-map? cfg)
@@ -912,7 +837,7 @@
:modified-at (ct/now)
:has-media-trimmed false}))))
(defn- absorb-library
(defn- absorb-library*
"Find all files using a shared library, and absorb all library assets
into the file local libraries"
[cfg {:keys [id data] :as library}]
@@ -927,10 +852,10 @@
:library-id (str id)
:files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file! cfg data) ids)
(run! (partial absorb-library-by-file cfg data) ids)
library))
(defn absorb-library!
(defn absorb-library
[{:keys [::db/conn] :as cfg} id]
(let [file (-> (bfc/get-file cfg id
:realize? true
@@ -947,7 +872,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file)))
(absorb-library cfg file)))
(absorb-library* cfg file)))
(defn- set-file-shared
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@@ -960,14 +885,14 @@
;; file, we need to perform more complex operation,
;; so in this case we retrieve the complete file and
;; perform all required validations.
(let [file (-> (absorb-library! cfg id)
(let [file (-> (absorb-library cfg id)
(assoc :is-shared false))]
(db/delete! conn :file-library-rel {:library-file-id id})
(db/update! conn :file
{:is-shared false
:modified-at (ct/now)}
{:id id})
(select-keys file [:id :name :is-shared]))
file)
(and (false? (:is-shared file))
(true? (:is-shared params)))
@@ -1014,6 +939,11 @@
{:id file-id}
{::db/return-keys [:id :name :is-shared :deleted-at
:project-id :created-at :modified-at]})]
;; Remove all possible relations for that file
(db/delete! conn :file-library-rel
{:library-file-id file-id})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
@@ -1075,19 +1005,19 @@
"Link a file to a library. Returns the recursive list of libraries used by that library"
{::doc/added "1.17"
::webhooks/event? true
::sm/params schema:link-file-to-library}
[cfg {:keys [::rpc/profile-id file-id library-id] :as params}]
::sm/params schema:link-file-to-library
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
(when (= file-id library-id)
(ex/raise :type :validation
:code :invalid-library
:hint "A file cannot be linked to itself"))
(db/tx-run! cfg
(fn [{:keys [::db/conn]}]
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(link-file-to-library conn params)
(bfc/get-libraries cfg [library-id]))))
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(link-file-to-library conn params)
(bfc/get-libraries cfg [library-id]))
;; --- MUTATION COMMAND: unlink-file-from-library
@@ -1107,8 +1037,9 @@
::webhooks/event? true
::sm/params schema:unlink-file-to-library
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(unlink-file-from-library conn params)
nil)
@@ -1132,8 +1063,9 @@
{::doc/added "1.17"
::sm/params schema:update-file-library-sync-status
::db/transaction true}
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id library-id] :as params}]
(check-edition-permissions! conn profile-id file-id)
(check-edition-permissions! conn profile-id library-id)
(update-sync conn params))
;; --- MUTATION COMMAND: ignore-sync
@@ -1164,47 +1096,53 @@
;; --- MUTATION COMMAND: delete-files-immediatelly
(def ^:private sql:delete-team-files
"UPDATE file AS uf SET deleted_at = ?::timestamptz
FROM (
SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])
) AS subquery
WHERE uf.id = subquery.id
RETURNING uf.id, uf.deleted_at;")
(def ^:private sql:get-delete-team-files-candidates
"SELECT f.id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
JOIN team AS t ON (t.id = p.team_id)
WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])")
(def ^:private schema:permanently-delete-team-files
[:map {:title "permanently-delete-team-files"}
[:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]])
(defn- permanently-delete-team-files
[{:keys [::db/conn]} {:keys [::rpc/request-at team-id ids]}]
(let [ids (into #{}
d/xf:map-id
(db/exec! conn [sql:get-delete-team-files-candidates team-id
(db/create-array conn "uuid" ids)]))]
(reduce (fn [acc id]
(events/tap :progress {:file-id id :index (inc (count acc)) :total (count ids)})
(db/update! conn :file
{:deleted-at request-at}
{:id id}
{::db/return-keys false})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at request-at
:id id}})
(conj acc id))
#{}
ids)))
(sv/defmethod ::permanently-delete-team-files
"Mark the specified files to be deleted immediatelly on the
specified team. The team-id on params will be used to filter and
check writable permissons on team."
{::doc/added "2.12"
::sm/params schema:permanently-delete-team-files
::db/transaction true}
{::doc/added "2.13"
::sm/params schema:permanently-delete-team-files}
[{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}]
(teams/check-edition-permissions! conn profile-id team-id)
(reduce (fn [acc {:keys [id deleted-at]}]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at deleted-at
:id id}})
(conj acc id))
#{}
(db/plan conn [sql:delete-team-files request-at team-id
(db/create-array conn "uuid" ids)])))
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
(teams/check-edition-permissions! pool profile-id team-id)
(sse/response #(db/tx-run! cfg permanently-delete-team-files params)))
;; --- MUTATION COMMAND: restore-files-immediatelly
@@ -1268,7 +1206,7 @@
{:keys [files projects]}
(reduce (fn [result {:keys [id project-id]}]
(let [index (-> result :files count)]
(events/tap :progress {:file-id id :index index :total total-files})
(events/tap :progress {:file-id id :index (inc index) :total total-files})
(restore-file conn id)
(-> result
@@ -1291,7 +1229,7 @@
(sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective
projects) on the specified team."
{::doc/added "2.12"
{::doc/added "2.13"
::sse/stream? true
::sm/params schema:restore-deleted-team-files}
[cfg params]
@@ -71,7 +71,7 @@
{::doc/added "1.20"
::sm/params schema:restore-file-snapshot
::db/transaction true}
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
[{: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)
(let [file (bfc/get-file cfg file-id)
team (teams/get-team conn
@@ -88,7 +88,8 @@
;; Send to the clients a notification to reload the file
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-restore
:message {:type :file-restored
:session-id session-id
:file-id (:id file)
:vern vern})
nil)))
Loaded 100 of 1581 files, more files were not shown because too many files have changed in this diff. Show more