2219 Commits

Author SHA1 Message Date
Andrey Antukh
d620c86053 Merge remote-tracking branch 'origin/staging' into develop 2026-05-15 11:58:06 +02:00
Andrey Antukh
6ac8012258 Merge remote-tracking branch 'origin/main' into staging 2026-05-15 11:57:16 +02:00
Andrey Antukh
3db0e5ee0d 📎 Update changelog 2026-05-15 11:31:58 +02:00
andrés gonzález
27ac0b7469
🐛 Unify layout creation telemetry for plugins and MCP (#9654)
* 🐛 Unify layout creation telemetry for plugins and MCP

* 📚 Update changelog for version 2.15.4

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

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-15 10:53:43 +02:00
Rene Arredondo
de1c942292
🐛 Use copia not copiar for Spanish duplicate-suffix (#9671) 2026-05-15 10:24:42 +02:00
andrés gonzález
310bf6fd6a
💄 Change auth illustration (#9552)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 16:25:53 +02:00
andrés gonzález
7e7bf7c458
Update Open Graph and link preview metadata (#9557)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 16:23:57 +02:00
Pablo Alba
5dafd44966
🐛 Fix library update button freezes and does not apply updates (#9513)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-14 16:03:47 +02:00
Andrey Antukh
9021544c05 Merge remote-tracking branch 'origin/main' into staging 2026-05-14 15:24:29 +02:00
Andrey Antukh
05d40e3370 📚 Update changelog 2026-05-14 15:19:24 +02:00
Andrey Antukh
fd19bf121f 📎 Update changelog 2026-05-14 14:23:59 +02:00
Andrey Antukh
52588412c7 Merge remote-tracking branch 'origin/staging' into develop 2026-05-14 11:12:01 +02:00
Andrey Antukh
d78074307f Merge remote-tracking branch 'origin/main' into staging 2026-05-14 11:07:42 +02:00
Milos Milic
884b125cf5
🐛 Fix two plugin error i18n keys broken by leading whitespace in en.po (#9501) 2026-05-13 15:59:04 +02:00
Pablo Alba
bf880467b4
🐛 Fix dependency libraries visible in UI after unlinking main library (#9511) 2026-05-13 15:35:42 +02:00
Alejandro Alonso
1a3b057814 🐛 Fix atlas corruption when dragging large shapes after zoom change 2026-05-13 13:00:56 +02:00
Pablo Alba
fffafdab93
🐛 Fix library updates reappear after file is reloaded (#9563)
* 🐛 Fix library updates reappear after file is reloaded

Summary
Migrate synced_at timestamps to a standalone file_library_sync table to ensure sync state is tracked for both direct and transitive libraries.

Problem
Transitive libraries (libraries imported by other libraries) are not stored as direct rows in file_library_rel. Because the system previously coupled synced_at directly to the file_library_rel schema, transitive libraries lacked a persistent location for their sync timestamps. This caused sync states to be lost or incorrectly reported for nested dependencies.

Changes
Schema Migration: Created file_library_sync and migrated existing synced_at values from file_library_rel.

Decoupling: Removed tight Foreign Key coupling to allow sync rows to exist independently of specific relationship records.

Persistent Writes: Added upsert-file-library-sync! helper. Updated all import, duplication, and RPC write paths (v1/v2/v3 importers, link-file-library) to ensure every write persists a sync row.

Unified Reads: Updated both direct and recursive/transitive library queries to fetch synced_at from the new table.

Testing: Added regression tests to verify that sync rows are correctly created/updated even when a transitive relation is absent in file_library_rel.

Impact
This fix ensures that the system accurately records and retrieves sync states for the entire library dependency tree, resolving the bug where nested libraries appeared out of sync.

*  MR review
2026-05-13 11:29:05 +02:00
Andrey Antukh
382efe3449 📚 Update changelog 2026-05-12 23:33:39 +02:00
Andrey Antukh
e5c99231da 📚 Update changelog 2026-05-12 18:43:08 +02:00
Andrey Antukh
ade0d2d0a8 📎 Update changelog with PR info 2026-05-12 13:01:42 +02:00
Pablo Alba
269edcd0ee
🐛 Fix restore saved version keeps view-only (#9514)
* 🐛 Fix restore saved verrsion keeps view-only

* 📎 Remove outdated note from CHANGES.md

Remove note about restoring saved version from Preview mode.

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

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-12 10:32:12 +02:00
Andrey Antukh
bd3ca6f8e5 📚 Update changelog 2026-05-12 09:33:33 +02:00
Jack Storment
c394a281c8
🐛 Revert blend-mode hover preview when dismissing dropdown (#9237)
* 🐛 Revert blend-mode hover preview when dismissing dropdown

When the blend-mode dropdown was dismissed by clicking outside instead
of selecting an option, the canvas kept rendering the last hovered
blend mode even though the inspector and data state had reverted. The
visible state of the shape no longer matched its stored state. Reset
the canvas render back to the shape's saved blend mode on dropdown
close so the preview never outlives the dropdown.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* Fix blend-mode dropdown and various bugs

Fix blend-mode dropdown behavior and revert WASM render on pointer leave. Also, address multiple bugs related to user interactions and data handling.

Signed-off-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
Co-authored-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>
2026-05-12 08:17:07 +02:00
Leona Lee
02bbbae0b0
🐛 Fix typography removal from plugin API (#9279)
* 🐛 Fix typography removal from plugin API

* 🔥 Remove unrelated delete-typography test

---------

Signed-off-by: Leona Lee <63717587+leonaIee@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:58:42 +02:00
dhgoal
3094d512f4
🐛 Expose Source Sans Pro 600 weight in builtin fonts list (#9247)
The font-family list at frontend/src/app/main/fonts.cljs registers
Source Sans Pro variants for weights 200, 300, 400, 700 and 900, but
omits the semibold (600) entries even though the font assets are
already bundled (frontend/resources/fonts/sourcesanspro-semibold.*)
and the CSS @font-face declarations that load them are present
(frontend/resources/styles/common/dependencies/fonts.scss:55-56).

Result: weight 600 cannot be selected from the font picker even
though the bytes are downloadable; users see a 400 -> 700 jump.

Add the two missing variant entries (600 and 600 italic) using the
same :suffix style as the other numeric-id entries (200, 300), since
the file-name component "semibold" doesn't match the weight number.

Issue mentions weights 500 and 800 as also missing, but no
sourcesanspro-medium or sourcesanspro-extrabold assets exist in the
repo, so this PR scopes to weight 600 only — the recoverable subset.

Closes #7378.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:55:25 +02:00
FairyPiggyDev
09bd7f96f6
Add author, relative timestamp and short identifier to history entries (#9132)
The workspace Actions history panel previously showed the operation
icon and a one-line message for each undo entry with no indication
of when the action happened, any stable way to refer to it, or who
made the change. The reporter of issue #7660 (and @Takhoffman's
follow-up comment) asked for a git-like display: `<hash> · <time> by
<name>`.

This change stamps each undo entry with its creation timestamp and
author at the moment it lands on the undo stack and surfaces three
extra pieces of information in the history sidebar:

- A short 7-character identifier derived from the entry's existing
  `:undo-group` UUID. Hovering shows the full UUID.
- A relative timestamp (e.g. `just now`, `5 minutes ago`, `2 hours
  ago`, `3 days ago`) rendered via `app.common.time/timeago` so it
  matches the formatting already used for comments and the dashboard.
- The display name of the profile that created the entry, rendered
  as `by <Name>` in the same metadata row.

The undo stack is client-side per profile, so every entry is always
the current user; the author is stored on the entry anyway so the UI
does not need to reach into profile state while rendering and so the
data stays correct if the stack shape ever changes.

Changes at a glance:

- `data/workspace/undo.cljs`: extend `schema:undo-entry` with an
  optional `:timestamp` and `:by`; new `profile-display-name` helper
  that falls back from full name to email to nil; `stamp-entry` now
  takes state and fills in both fields on entries that do not
  already carry them. Pre-stamped entries (e.g. coming out of an
  accumulated transaction) keep their original values.
- `ui/workspace/sidebar/history.cljs`: propagate `:timestamp`,
  `:undo-group`, and `:by` through `parse-entries`; add `short-id`
  helper; render the metadata row in `history-entry` using
  `app.common.time/timeago` against `:timestamp`; skip the author
  span entirely when `:by` is nil.
- `ui/workspace/sidebar/history.scss`: styling for the new metadata
  row (monospace hash, muted separator, truncated time/author).
- `translations/en.po`: 1 new string for `by %s`.

Existing undo entries created before this change have neither
timestamp nor author; the UI is defensive about both, so old entries
simply render with whatever data they have (and often the plain
title on its own).

Github #7660

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 14:48:32 +02:00
Andrey Antukh
06986e25a3 Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 14:06:31 +02:00
Andrey Antukh
6ef231bf38 Merge remote-tracking branch 'origin/main' into staging 2026-05-11 14:04:27 +02:00
bitloi
58ca0a16ba
🐛 Fix MCP SSE sessions leaking on zombie connections (#9432) (#9464)
SSE sessions were never included in the periodic inactivity timeout
checker, so a stale connection whose TCP close event never fired would
retain its SSEServerTransport and McpServer indefinitely.

Changes:
- Add lastActiveTime: number to the sseTransports entry type
- Initialise lastActiveTime at SSE session creation (GET /sse)
- Refresh lastActiveTime on every incoming message (POST /messages)
- Extend startSessionTimeoutChecker() to sweep and forcibly close SSE
  sessions idle for more than SESSION_TIMEOUT_MINUTES, mirroring the
  existing Streamable HTTP logic
- Update the checker log to count both transport maps

The existing res.on('close') cleanup path is preserved unchanged:
it remains the primary cleanup for normal disconnections; the timer
is a safety net for zombie sessions only.

Closes #9432

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 13:55:11 +02:00
Milos Milic
b54fa2f11c
🐛 Reject clipboard helpers gracefully on insecure origins (#9188)
* 🐛 Reject clipboard helpers gracefully on insecure origins

Closes #6514. Resolves the user-visible crash originally reported
in #4478.

`app.util.clipboard/to-clipboard` and `to-clipboard-promise` called
`(unchecked-get js/navigator "clipboard")` and then immediately
invoked `.writeText` / `.write` on the result, with no guard for the
case where `navigator.clipboard` is `undefined`. The W3C Clipboard
API spec requires a "secure context" (HTTPS or localhost), so a
Penpot instance served over plain HTTP - which the SSDP/LAN
self-hosted setup in #4478 was - throws

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

synchronously the moment the user clicks any copy button. The error
escapes the consuming function before any error-handling rx/of arm
runs, so the whole UI ends up on the error screen instead of just
the affected control showing a "could not copy" message.

A third helper (`to-clipboard-multi`) already guards `clipboard` and
`clipboard.write`, but if both are missing it silently returns nil
which is also surprising for callers expecting a Promise.

## Fix

Add a small `get-clipboard` accessor and an `unavailable-error`
factory that returns `Promise.reject(Error(...))` with a clear
message ("Clipboard API is unavailable. This usually happens when
the page is served over plain HTTP; serve Penpot over HTTPS to
enable copy-to-clipboard."). Wire all three helpers through the
same defensive contract:

- `to-clipboard` - return the rejected Promise when
  `navigator.clipboard.writeText` is missing.
- `to-clipboard-promise` - return the rejected Promise when
  `navigator.clipboard.write` is missing.
- `to-clipboard-multi` - convert the existing `if` into a `cond`
  with three branches: prefer `clipboard.write` for true multi-MIME
  output, fall through to `writeText` with the text/plain payload
  when only the legacy text path is available, and finally reject
  with the unavailable error when neither path exists. Previously
  the no-API case fell off the `when-let` and silently returned
  nil.

The contract is now consistent: every helper either resolves or
rejects a Promise, never throws synchronously, and never returns
nil. Callers (which are already structured around rx streams that
call `rx/from` on the helper's return value) can chain `.catch` /
`rx/catch` to surface a status-bar message instead of crashing.

The two stale `;; FIXME` comments on `to-clipboard` (rename to
`write-text`) and `to-clipboard-promise` (this API is very confuse)
are removed - the rename remains an open follow-up across 13+ call
sites and is intentionally out of scope, but the API is no longer
"confuse" once the contract is documented and uniform.

CHANGES.md entry added under the 2.17.0 Bugs-fixed section
describing the user-visible behaviour change.

* 🐛 Reject paste-from-navigator gracefully on insecure origins

Symmetric companion to the to-clipboard / to-clipboard-promise /
to-clipboard-multi guards added earlier in this PR. The paste path
went through fromNavigator (frontend/src/app/util/clipboard.js) which
called `navigator.clipboard.read()` with no nil-check; on insecure
origins (plain HTTP / non-localhost) this raised an opaque
`TypeError: Cannot read properties of undefined (reading 'read')` and
the workspace surfaced a generic 'Something wrong has happened' toast
instead of the descriptive 'serve Penpot over HTTPS …' message users
get for the copy direction.

Mirror the get-clipboard pattern from clipboard.cljs:
- Read `navigator.clipboard` once into a local.
- If it's missing the `.read` method, throw a descriptive Error that
  matches the wording the copy direction already uses (only the verb
  swaps: 'paste-from-clipboard' instead of 'copy-to-clipboard').
- Otherwise, dispatch through the local handle.

The existing app.util.clipboard/from-navigator (clipboard.cljs:32)
already wraps impl/fromNavigator in rx/from, so a rejected Promise
from the async function propagates as an rx error event. Existing
callers that subscribe with .catch / on-error see the structured
Error and surface the toast, identical to how to-clipboard's
unavailable-error already flows.

Repro (matches niwinz's reproduction in the PR comment):

  Object.defineProperty(navigator, 'clipboard', { value: undefined });
  // … then attempt a paste action in the workspace …

Before: TypeError in console + 'Something wrong has happened' toast.
After: descriptive Error caught by the rx subscription and rendered
through the existing unavailable-Clipboard-API surface.

Refs #6514, #4478

* 🐛 Show user-facing toast when clipboard API is unavailable

Niwinz's review on penpot#9188 caught that the rejected Promise from
to-clipboard / to-clipboard-promise / to-clipboard-multi /
fromNavigator now surfaces the correct error to the console, but the
workspace UI still falls through to the generic "Something wrong has
happened" toast because the on-clipboard-permission-error and the
paste error-handler in paste-from-clipboard only branched on
clipboard-permission-error?.

Apply the patch he suggested in the review:

- Add clipboard-unavailable-error? predicate that matches the
  Promise.reject(Error("Clipboard API is unavailable. ...")) thrown
  by the get-clipboard / unavailable-error helpers added earlier in
  this PR. Uses str/starts-with? on the message prefix so the
  predicate stays stable even if the trailing "serve Penpot over
  HTTPS ..." advice text is reworded later.
- Convert on-clipboard-permission-error from `if` to `cond` and add
  a third arm that fires errors.clipboard-api-unavailable as a
  warning toast.
- Add the same arm in the second cond block inside
  paste-from-clipboard, before the :not-implemented and :else arms.
- Add the matching errors.clipboard-api-unavailable entry to
  frontend/translations/en.po with the wording niwinz suggested:
  "Clipboard API is unavailable. Serve Penpot over HTTPS to enable
  clipboard access".

Refs penpot#9188 review.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-11 13:52:36 +02:00
Andrey Antukh
b312e6b059 📚 Update changelog 2026-05-11 11:26:54 +02:00
Andrey Antukh
08bd53b6a1 📎 Update changelog 2026-05-11 09:49:28 +02:00
Andrey Antukh
a228b257e9 Merge remote-tracking branch 'origin/staging' into develop 2026-05-11 09:37:32 +02:00
Andrey Antukh
f2c631b8b7 Merge remote-tracking branch 'origin/main-staging' into staging 2026-05-11 09:30:10 +02:00
Andrey Antukh
1a212a2769 Merge remote-tracking branch 'origin/main-staging' 2026-05-11 08:46:25 +02:00
Andrey Antukh
6eba2e6c42 📚 Update changelog 2026-05-10 20:23:10 +02:00
Jack Storment
9dc607902b
🐛 Only fall back to anonymous on :not-found in get-profile (#9254)
* 🐛 Only fall back to anonymous on :not-found in get-profile

::get-profile caught Throwable and silently returned the anonymous
user payload for every error - contradicting the in-code comment that
states in all other cases we need to reraise the exception. Under
transient DB conditions (pool checkout timeout, replica lag, statement
timeout, network blip) this masked real DB outages as ordinary
anonymous responses, returning HTTP 200 instead of 5xx and leaving
logged-in users on the login screen with a valid session cookie.

Narrow the catch so only :type :not-found falls through; everything
else propagates and reaches the standard error pipeline.

Closes #9235

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 🐛 Only fall back to anonymous on :not-found in get-profile

::get-profile caught Throwable and silently returned the anonymous
user payload for every error - contradicting the in-code comment that
states in all other cases we need to reraise the exception. Under
transient DB conditions (pool checkout timeout, replica lag, statement
timeout, network blip) this masked real DB outages as ordinary
anonymous responses, returning HTTP 200 instead of 5xx and leaving
logged-in users on the login screen with a valid session cookie.

Narrow the catch so only :type :not-found falls through; everything
else propagates and reaches the standard error pipeline.

Closes #9253

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
2026-05-10 19:40:29 +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
60c718eba1 Merge remote-tracking branch 'origin/staging' into develop 2026-05-10 09:20:27 +02:00
Andrey Antukh
f414392f13 📎 Update changelog 2026-05-10 09:19:56 +02:00
Andrey Antukh
cf3455a487 📎 Add missing entry on CHANGES.md 2026-05-10 09:18:52 +02:00
Andrey Antukh
10a23a6869 Merge remote-tracking branch 'origin/main' into staging 2026-05-10 09:16:41 +02:00
Renzo
ea24445c2c
🐛 Toggle display-guides via physical key code so the shortcut works on non-US layouts (#9209)
* 🐛 Toggle display-guides via physical key code so the shortcut works on non-US layouts

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

* 🐛 Add tests for display-guides shortcut on non-US layout

---------

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Renzo <170978465+RenzoMXD@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 17:36:52 +02:00
BitToby
6aeccb1208
🎉 Add selection size badge below bounding box (#9210)
* 🎉 Add selection size badge below bounding box

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>

* 💄 Address review comments

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>

* 💄 Move selection size badge text styles to SCSS class

---------

Signed-off-by: bittoby <218712309+bittoby@users.noreply.github.com>
Signed-off-by: BitToby <218712309+bittoby@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 14:20:24 +02:00
web-dev0521
bb93928099
🐛 Fix lost-update race on team.features during concurrent file cr… (#9198)
* 🐛 Fix lost-update race on team.features during concurrent file creation

* 📚 Add CHANGES.md entry for team.features race condition fix (#9197)
2026-05-08 14:12:20 +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