3367 Commits

Author SHA1 Message Date
Andrey Antukh
fffc3ee222 Merge remote-tracking branch 'origin/staging' into develop 2026-10-19 22:18:27 +02:00
Pablo Alba
c7f16d3236
🐛 Keep accented letters in organization avatar initials (#12328)
get-initials used an ASCII-only [a-zA-Z] match, so names like Ábaco
and Órbita skipped the leading accented letter. Match Unicode letters
with \p{L} (CLJS RegExp + u/g flags) so org default avatars match the
expected first letter. Closes #12324.

AI-assisted-by: Cursor <cursoragent@cursor.com>
2026-10-19 15:56:05 +02:00
Eva Marco
4c655e6e9e
🐛 Keep the picture in place in the SVG preview of an image crop (#12387)
With the SVG renderer, resizing a shape with an image fill and the mod
key showed the picture scaled during the drag, as in a normal resize.
The crop only showed after releasing the mouse.

The SVG preview applies the resize matrix to the shape node and ignores
the fill transform change carried by the modifiers. Now the pictures of
the shape fill patterns get a matrix that takes them from the old fill
transform to the new one, which undoes that resize and keeps them in
place. This works for shapes and boards.

Closes #12349

AI-assisted-by: claude-opus-5-5
2026-10-19 13:40:26 +02:00
makesomethingshit
91a5b3a9fb
🐛 Fix token tree path handling for repeated and nested segments (#11591)
* 🐛 Fix token tree collapsing repeated path segments

Tokens whose path contains the same segment name twice in a row
(e.g. test.pointer.pointer) collapsed to a single level in the
token tree.

build-tree-node marked a node as a leaf when the current segment
name matched the first segment of every remaining path. Because
group-by-first-segment rewrites nested paths into the segment
name, a still-nested remaining path was indistinguishable from a
token ending at the current node, so test.pointer.pointer merged
into one leaf.

Mark rewritten segments with an internal ::remaining? marker and
strip it when the rest-path is fully consumed; is-leaf? only
accepts unmarked remaining segments as leaves.

Closes #11588

AI-assisted-by: glm-5.3-flash
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Preserve ancestor tokens in path trees

Keep a node's token and descendants independently when imported paths
contain both an ancestor and a child. Render both in the token tree and
include the ancestor token in existing path-based group operations.

Cover accepted same-set imports, leaf IDs, repeated segments, and internal
marker removal without changing import or collision validation.

AI-assisted-by: GPT-6
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

* ♻️ Rework token tree path building

Carry the segments left to descend as a ::remaining-path vector on each
segment and advance it by subvec, instead of rewriting the segment :name
and re-splitting it later.

With that, build-tree-node finds the leaf token with a predicate over
::remaining-path and takes children from the segments that still have
one, so the pre-grouping children-first sort no longer changes the
result. Drop sort-children-first and its segment-has-children? helper,
and document inside-path?.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Fix and relocate the token tree tests

The ancestor-and-descendant test called a function that does not exist
in app.common.types.tokens-lib, so the namespace failed to load and the
whole common suite errored. Use make-tokens-status-from-lib and the
two-argument get-tokens-in-active-sets instead.

Move the build-tree-root tests to path-names-tree-test so
path-names-test stays a pure split/join unit test.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Refactor the token tree rendering and share path helpers

Extract the token pill into a leaf-node* component and treat a node as a
leaf only when it holds a token and has no children, so a node can carry
both a token and its children.

Make token-separator public and reuse it from the token tree and the
management panel instead of local copies. Both management path filters
inlined the same ancestor-or-descendant rule; extract it as
token-in-path? and cover it with a unit test.

AI-assisted-by: deepseek-v4.1-flash

---------

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: luisddm <luis.dedios@kaieldos.net>
2026-10-19 09:44:32 +02:00
Andrey Antukh
8c5113b815 Merge remote-tracking branch 'origin/staging' into develop 2026-10-17 14:44:44 +02:00
Andrey Antukh
d1aa36b814 ⬆️ Update pnpm to 12.14.0 and Node to v24.22.0
Bump the pnpm pin to 12.14.0 in the devenv and the production images
and stamp the same version into every package.json, realigning the
drift between the devenv (12.9.1) and the rest (12.6.0).

Bump Node to v24.22.0, the newest patch of the Krypton LTS line, in
.nvmrc, the devenv and image Dockerfiles and the plugins-deploy
workflows.

The pnpm arch checksums are those of the GitHub release tarballs, not
of the npm package. Lockfile changes are metadata only.

AI-assisted-by: deepseek-v4.1-flash
2026-10-17 14:00:43 +02:00
Alonso Torres
02e10a005a
✨ Add token source to plugins (#12183)
* ✨ Add token source to plugins

* ✨ Changes after review

* ✨ Move plugins code to library-edit
2026-10-16 17:58:51 +02:00
Andrey Antukh
03134d35ae
🐛 Fix silent failure when uploading custom fonts (#12315)
* 🐛 Fix silent failure when uploading custom fonts

Custom font upload crashed with re-seq must match
against a string when opentype.js returned nil
subfamily names, leaving the user with no feedback.
Unknown signatures and unparseable files were also
silently dropped.

Make variant parsing nil-safe, harden entry
preparation for missing tables, report every
per-file failure with a bad-font notification, and
show a generic error if the stream itself fails.

Closes #12304

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Rename font mtype helper and mark helpers private

Avoid confusion with app.worker.import/parse-mtype,
which detects binfile signatures. The font helper now
lives as parse-font-mtype, and the upload helpers only
used internally are private, with tests reaching them
through var references.

AI-assisted-by: muse-spark-1.3-contributor

* 💄 Fix formatting in font upload tests

Realign map keys shifted by the var-quote prefix so
cljfmt check passes again. No behavior change.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix infer warnings in fonts metrics code

Add missing ^js type hints to the hhea and os2 metric lookups in
prepare-font so shadow-cljs can infer the interop targets and the
release build reports zero warnings.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-16 12:51:48 +02:00
Andrey Antukh
dfc55a7192 Merge remote-tracking branch 'origin/staging' into develop 2026-10-16 12:48:49 +02:00
Andrés Moya
a5f75c7394
🐛 Fix repair bad variants (#11983)
* 🐛 Ensure normalized names in components and variants

We have a rule of that shape names that include / separator should have
it normalized, with one space before and one after. But this rule is
being enforced only in the UI and the plugins API. There may be other
ways of creating shapes that does not follow it.

For normal shapes this is not really a problem. But for main instances
it may cause that the name of the shape does not match the joined
path+name of the component, and this may generate some problems.

We have decided to add a migration to fix this in all main instances and
variant containers and enforce it in the future with integrity
validations (normal shapes may be left as is).

* 🐛 Fix bad variant-id in the component and the main instance

* 🔧 Fix some issues detected in PR review

* 🐛 Add nil guard
2026-10-16 10:59:57 +02:00
Andrés Moya
feb73f0fd8
✨ Handle tokens libraries when exporting .penpot files (#12077)
* ✨ Handle tokens libraries when exporting .penpot files

Exporting a file that takes its tokens from an external library left
the :tokens-source of the file pointing at a library that is not in the
export, and the tokens lib of that library was not exported.

With :detach-libraries, unset the :tokens-source and the tokens status
in detach-external-references. The file is exported without any link to
the library, and the shapes keep their applied tokens.

With :merge-libraries, add cfo/absorb-tokens-lib, which copies the tokens
lib of a library into a file and unsets its :tokens-source, so the file
becomes its own tokens source. The tokens status of the file is kept; if
it has none, the one of the library is copied. If the library has no
tokens lib, only the source is unset. embed-assets calls it for the
library that is the tokens source of the file, and ignores the other
linked libraries.

Add unit tests for absorb-tokens-lib, and binfile tests that export a
file with tokens from an external library with both modes, checking the
exported files, the tokens lib, the tokens source and the applied
tokens.

Also reformat the export-type docstring in binfile v3 and remove a
leftover debug prn in the export dialog.

AI-assisted-by: claude-sonnet-5-5

* 🐛 Preserve active token sets when importing linked libraries

Cache each decoded tokens library for the duration of an import so files
and their tokens source resolve active sets against the same generated IDs.
Add a round-trip regression test for exports that include linked libraries.

* 🐛 Preserve tokens when deleting or unpublishing libraries

Absorb the tokens library into files that use it as their tokens source.
Keep referencing file IDs in the deletion task before removing relations.

Embed tokens from deleted, unlinked sources during export, and clear stale
source and status data when the library no longer exists. Add regression
tests for deletion, unpublishing, and exports with unavailable libraries.

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-15 16:53:30 +02:00
Eva Marco
98f8b7e625
🐛 Keep the picture in place when a crop crosses the opposite edge (#12361)
With the mod key, resizing a shape with an image fill crops it: the
shape is a window over a picture that keeps its size and position.
When the dragged edge crossed the opposite one, the crop mode turned
off and a normal resize with a negative scale flipped the shape. The
image transform is relative to the shape, so the picture was mirrored
and shrank a bit more on every crossing until it disappeared.

Keep the crop mode when the edge crosses: the window moves to the
other side of the fixed edge without flipping, the part outside the
picture shows empty, and moving the window back shows the picture
again. The crop math lives in app.common.geom.image-crop and replaces
the image transform code that was duplicated for fills and fill-image.

Closes #12349

AI-assisted-by: claude-opus-5-5
2026-10-15 16:23:32 +02:00
Eva Marco
0d9b1c48fc
🐛 Fix legacy path migration crash on paths without points (#12357)
The legacy-6 migration read the rect of each path from its points and
checked whether the width or height was close to zero. A path with
missing or empty points has no rect, so the check got nil and threw a
NullPointerException. The migration then failed on every load of that
file, and get-team-shared-files returned 500 for the whole team, so the
"Add library" modal could not open.

Treat a missing rect like a degenerate one and rebuild the selrect and
points from the path content.

Closes #12343

AI-assisted-by: claude-opus-5-5
2026-10-15 13:23:54 +02:00
Andrey Antukh
c74d415ddd
🐛 Stop oversized audit events from wedging the archive queue (#12252)
* 🐛 Stop the export-shapes modal event from dumping all exports

The `:export-shapes` modal event carried the full `:exports` vector,
each export embedding its shape (path data, geometry, text content).
One event reached 1.7 MB, which made the audit archive cron POST a
chunk past the proxy body limit and wedge the queue.

Build a compact body for that modal instead: total exports, distinct
shapes and one count per export type, plus the page name. Other modals
keep their props unchanged.

Move the modal `:origin` prop to `::ev/origin` so `make-proto-event`
records it on the event context as `:event-origin`; the UI still reads
the plain prop from the raw state.

Add tests for the summary and for the modal origin.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Bound exception messages in reports and audit events

A frontend crash whose message embeds a whole payload (a serialized
file) produced a 9.4 MB audit event: the message was repeated in the
`:hint` prop, in the report context and in the stack trace. A single
event that big wedged the audit archive queue.

Bound exception messages at the source: `get-hint` clamps to
`max-message-length` (1000 chars), and the CLJS `format-throwable`
clamps the message line of the stack trace and honors `:trace-length`
(parity with the CLJ version). `report-context` now prints `get-hint`
instead of the raw `:hint` from `ex-data`.

Fix `first-line` on the JVM: `cuerdas/index-of` returns nil when the
substring is missing and `(pos? nil)` throws there (it is false in
CLJS), so single-line messages crashed the backend path.

AI-assisted-by: deepseek-v4.1-flash
2026-10-15 10:19:26 +02:00
Alonso Torres
286a6d5c9a
✨ Add new dashboard methods to the API (#12169)
* ✨ Add new dashboard methods to the API

* 🐛 Fix some issues with the new methods

* 🐛 Fix problem with mcp test
2026-10-14 15:24:53 +02:00
Andrey Antukh
0c0336da42 Merge remote-tracking branch 'origin/staging' into develop 2026-10-13 21:04:58 +02:00
Eva Marco
317a731dda
🐛 Detect and repair children missing from their parent's list (#12216)
* 🐛 Detect and repair children missing from their parent's list

A shape could point to an existing parent that did not list it in its
:shapes. The validator missed it, since it only walks the tree from
the root plus the shapes whose parent does not exist. Operations that
walk :shapes missed it too: deleting a variant also deleted its
container, which looked empty, and left the other variants without a
parent, so the backend rejected the change and the file stopped
saving.

- get-orphan-shapes also returns shapes not listed by their parent,
  so :child-not-in-parent is reported.
- The :child-not-in-parent repair adds every unlisted child of the
  parent. All repairs share the same file data and each one sets the
  whole :shapes, so with several children only the last one was kept.
- New migration 0032-fix-children-not-in-parent repairs the affected
  files when they are opened.

Closes #11758

AI-assisted-by: claude-opus-5-5

* 🐛 List shapes moved to root by the parent-not-found repair

The :parent-not-found repair set the shape's parent to the root but
did not add it to the root's :shapes, so it left the shape in the
child-not-in-parent state that the validator now reports. It now
adds every shape without parent to the root at once, since all
repairs are built from the same file data and each one sets the
whole :shapes.

Also cover the missing parent case in the repair tests.

AI-assisted-by: claude-opus-5-5

* ♻️ Simplify unlisted children validation and repair

Address review comments on the validation and repair of shapes that
their parent does not list.

- get-orphan-shapes no longer rebuilds *children-sets*: both callers
  bind it. The lookup moves to a listed-in-parent? helper shared with
  check-parent-children, which scans :shapes when the var is unbound.
- The :child-not-in-parent and :parent-not-found repairs no longer
  walk every shape of the page. Each one moves only its own shape with
  pcb/change-parent. The :mov-objects change adds the shape to the
  parent's :shapes when applied, so several repairs on one parent no
  longer overwrite each other.
- The :child-not-in-parent repair test uses plain shapes instead of
  variants.

AI-assisted-by: claude-opus-5-5
2026-10-13 11:17:01 +02:00
Miguel de Benito Delgado
5afdeffa5f
♻️ Move frame type constants to webgl.cljs (#11949)
* ♻️ Move some constants out of app.render-wasm.api

- Moved to aapp.render-wasm.wasm where they can be imported
  without pulling half the editor with them
- Renamed one for consistency

* 🔥 Delete unused function, update docstring
2026-10-09 11:22:19 +02:00
Andrey Antukh
473a59b134 Merge remote-tracking branch 'origin/staging' into develop 2026-10-09 09:01:12 +02:00
Luis de Dios
fad083314d
🎉 Expose per-side stroke width in the plugin API (#11994)
* ✨ Add stroke side-width helpers to common types

Add set-width-to-all-sides, set-width-to-single-side and side-width to
app.common.types.stroke, mirroring the border-radius helpers in
app.common.types.shape.radius. They back the per-side stroke width API
and are covered by unit tests.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Round-trip per-side stroke widths through the plugin API

Add strokeWidthTop/Right/Bottom/Left to format-stroke and parse-stroke so
reading shape.strokes exposes the per-side widths and writing a stroke
back preserves them instead of collapsing to the uniform width.

parse-stroke prefers a live stroke proxy's hidden raw state when present,
so a commit only rewrites the attributes the plugin touched.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Add per-side stroke widths to the plugin API types

Add the four per-side members to the Stroke interface and to the
borderWidth and dimension token property lists, and refresh the generated
API surface used by the plugin test suite.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Expose per-side stroke width on the stroke proxy

Add strokeWidthTop/Right/Bottom/Left to StrokeProxy. Reads fall back to
the uniform strokeWidth; strokeWidth now writes all four sides, like
borderRadius writes all four corners; each side setter writes only its
own key and the top side also updates strokeWidth.

Reject per-side writes on shapes other than boards and rectangles with a
validation error naming the property, and expose the raw stroke map
through a hidden proxy member so a commit preserves untouched attributes.

AI-assisted-by: deepseek-v4.1-flash

* ✨ Support stroke width tokens per side in the plugin API

Token attribute expansion now accepts the uniform strokeWidth property and
expands it to the four per-side attributes, mirroring the design tab.
shape.tokens exposes the per-side keys and reports strokeWidth when all
four sides share one token, and applyToken routes through the shared
expander.

AI-assisted-by: deepseek-v4.1-flash

* 📚 Document per-side stroke width in the plugins changelog

Note the new Stroke members, the restored strokeWidth token property and
the shape-type validation in plugins/CHANGELOG.md.

AI-assisted-by: deepseek-v4.1-flash

* 🐛 Fix some minor issues

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-08 18:09:06 +02:00
Eva Marco
31a079a7a2
🐛 Allow creating tokens when the file is its own tokens source (#12182)
Setting the local file back as the tokens source, or unlinking the
library that was the source, stored the file's own id in
:tokens-source. ensure-tokens-lib only created a tokens-lib when
:tokens-source was nil, so a file without a tokens-lib never got one,
and every token change was dropped without any error.

Now ensure-tokens-lib also creates the tokens-lib when the source is
the file itself, which fixes files already in that state, and
set-tokens-source removes the attribute instead of storing the file's
own id.

Closes #12180

AI-assisted-by: claude-opus-5-5
2026-10-08 09:55:18 +02:00
Miguel de Benito Delgado
f252439d84
♻️ Factor GL context initialization out of canvas init (#11931)
* ♻️ Factor GL context initialization out of canvas init

- fixes setting of gl-context-handle to be after _init so an exception
  does not leave invalid state behind.
- api.init-canvas-context uses a new webgl.init-context!
- this decouples config from init, so we can reuse it in benchmarks
- some helpers move to the webgl ns

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3

* 🐛 Release acquired GL context when renderer init fails

- Properly release resources if init fails
- Rethrown exceptions with the original cause
- Fix vacuous check in clear-canvas

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3

* ✨ Test webgl context init and release

AI-assisted-by: muse-spark, glm-5.3

* 💄 Address review comments

* 💄 Drop `!` from some function names
2026-10-07 18:19:34 +02:00
alonso.torres
dfff188409 ✨ Ask users to allow global plugins on install
A plugin with "scope": "global" keeps running across the dashboard
and files and can find and open the user's projects and files. Any
manifest could ask for this without the user being told.

Add the allow:global permission. The plugin registry adds it to
every manifest with global scope, so the install dialog lists it,
and a plugin that becomes global in a later version shows the
permissions update dialog again. The permission is valid in the
shared schema, which the backend checks when saving, and in the
runtime manifest schema. The built-in MCP plugin declares it too.

Document the scope property and the new permission for plugin
developers.
2026-10-07 14:58:32 +02:00
Dominik Jain
be17fabc6a 🎉 Introduce global plugins and move MCP to application scope
Global plugin lifecycle:
- Add optional manifest scope "global" for plugins that remain active
  across dashboard and workspace navigation within one browser tab.
  Existing plugins retain workspace scope by default.
- Preserve global sandboxes when a workspace ends or another plugin
  loads, including when that other plugin fails to initialize.
- Avoid duplicate instances of an already-running global plugin.
- Close global plugins on explicit closure or logout, with idempotent
  cleanup of listeners, timers and UI resources. Global scope does not
  imply automatic startup or persistence across a browser reload.

Plugin API and navigation:
- Keep a permanently live penpot facade whose getters follow the current
  workspace, while user, theme and UI facilities remain available on
  the dashboard.
- Expose penpotMgmt to global plugins with workspace metadata,
  none/loading/ready status and workspacechange subscriptions.
- Add awaitable openFile(fileId, {teamId}) navigation in the current tab,
  defaulting to the current team and resolving only once the requested
  file and its active page are ready.
- Reject invalid identifiers, failed or superseded navigation and waits
  exceeding 30 seconds; release navigation watchers on completion.
- Guard workspace-bound operations while no workspace is ready and
  return null from root, currentFile and currentPage in that state.
- Require content:read for the initial management API. Callers must
  reacquire file, page and shape references after changing files.

MCP lifecycle and connection controls:
- Make MCP a global plugin and initialize its integrated controller per
  authenticated application session instead of per workspace.
- Load the enabled integration with a valid token on the dashboard as
  well as in a file, retaining its existing hidden-iframe implementation.
- Keep enabling and connecting separate: an enabled plugin starts idle,
  and each tab connects explicitly through dashboard or workspace
  controls.
- Preserve the iframe, WebSocket, heartbeat and reconnect machinery
  during navigation; disconnect on request and close on disable/logout.

MCP sessions and execution:
- Derive the short MCP session ID solely from currentUser.sessionId,
  excluding the file ID. Retain the SHA-256-based, 50-bit Base32 encoding
  to produce a stable, copyable 10-character identifier.
- Allow sessions without an open file and update workspace metadata over
  the existing connection without replacing its routing identity.
- Refresh context after the connection handshake so initialization does
  not leave stale file metadata.
- Expose penpotMgmt to execute_code, preserve execution storage across
  navigation and skip layout waits when no workspace is ready.
- Update API types, tool guidance, documentation and the changelog.

Validation:
- Cover authenticated startup, workspace survival, logout cleanup,
  readiness, navigation failure, duplicate loads and session routing.
- Verify that short IDs remain stable across files and reconnects and
  that disconnect cancels pending asynchronous ID initialization.
- Pass 16 focused frontend tests, 73 runtime tests, 30 MCP plugin tests,
  builds, TypeScript checks and frontend/runtime lint.
- Pass all 73 MCP server tests with --test-force-exit; the normal runner
  still stays alive after assertions finish.
- Verify a live dashboard-to-file-to-dashboard round trip without a
  document or iframe reload, preserving facade identity and storage.

AI-assisted-by: gpt-6
2026-10-07 14:58:32 +02:00
Belén Albeza
1d7c775f72
🐛 Fix not setting mimetype in image fills (#12136) 2026-10-07 12:26:45 +02:00
Luis de Dios
f962cf487c
🐛 Migrate legacy stroke width token references (#12018)
Stroke width tokens used to be applied to the single `:stroke-width`
token attribute. Applying tokens per side replaced it with the four
`:stroke-width-*` attributes, but no migration moved the existing
references, so files written before 2.19 look unapplied: the token
pill stays off, the Design tab shows a plain number and propagation
finds no update action.

Add migration 0031, which copies the legacy reference to every side,
keeps any side reference already present and drops the old key. It
covers page and component shapes.

Closes #12016

AI-assisted-by: deepseek-v4.1-flash
2026-10-07 11:17:40 +02:00
Andrey Antukh
a5df69f60f Merge remote-tracking branch 'origin/staging' into develop 2026-10-07 10:25:01 +02:00
Andrey Antukh
b2d688a04c 💄 Stringify ids in comp-processors log props
Wrap every id prop in the comp-processors warn logs
with str so the log shows plain text instead of
the #uuid reader form. Also use :hint instead of
:msg and lowercase text, as the rest of the system
does. No logic changes, logging only.

AI-assisted-by: muse-spark-1.3
2026-10-07 08:17:33 +00:00
Andrey Antukh
fc8a126048 Merge remote-tracking branch 'origin/staging' into develop 2026-10-06 18:59:56 +02:00
Eva Marco
fcbb8f1fc0
🐛 Keep derived layout paddings non-negative (#12129)
Adding a flex or grid layout to a board with children derives the
paddings from the distance between the board and its children. A child
that overflows the board, or floating point noise after "Resize board
to fit content", gave negative paddings. The shape schema rejects them
since paddings must be non-negative, so update-file failed and autosave
stopped. Clamp the derived paddings to 0, as the derived gaps already
are.

Closes #12107

AI-assisted-by: claude-opus-5-5
2026-10-06 16:34:28 +02:00
Andrey Antukh
746184caf9 Merge remote-tracking branch 'origin/staging' into develop 2026-10-06 15:58:01 +02:00
Andrey Antukh
51ae83202e Merge remote-tracking branch 'origin/main' into staging 2026-10-06 15:56:55 +02:00
penpot-renovate[bot]
a61a4fceb1 ⬆️ Update dependency pnpm to v12.8.1
- pnpm 12.8.1 (docker/images/Dockerfile.media-processor)
- pnpm 12.8.1 (docker/images/Dockerfile.mcp)
- pnpm 12.8.1 (docker/images/Dockerfile.exporter)
- pnpm 12.8.1 (render-wasm/package.json)
- pnpm 12.8.1 (plugins/package.json)
- pnpm 12.8.1 (plugins/libs/plugins-styles/package.json)
- pnpm 12.8.1 (plugins/libs/plugins-runtime/package.json)
- pnpm 12.8.1 (plugins/libs/plugin-types/package.json)
- pnpm 12.8.1 (plugins/apps/table-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/rename-layers-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/poc-tokens-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/poc-state-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/plugin-api-test-suite/package.json)
- pnpm 12.8.1 (plugins/apps/lorem-ipsum-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/icons-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/example-styles/package.json)
- pnpm 12.8.1 (plugins/apps/e2e/package.json)
- pnpm 12.8.1 (plugins/apps/create-palette-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/contrast-plugin/package.json)
- pnpm 12.8.1 (plugins/apps/composable-test-suite/package.json)
- pnpm 12.8.1 (plugins/apps/colors-to-tokens-plugin/package.json)
- pnpm 12.8.1 (package.json)
- pnpm 12.8.1 (media-processor/package.json)
- pnpm 12.8.1 (mcp/packages/server/package.json)
- pnpm 12.8.1 (mcp/packages/plugin/package.json)
- pnpm 12.8.1 (mcp/packages/common/package.json)
- pnpm 12.8.1 (mcp/package.json)
- pnpm 12.8.1 (library/package.json)
- pnpm 12.8.1 (frontend/text-editor/package.json)
- pnpm 12.8.1 (frontend/packages/ui/package.json)
- pnpm 12.8.1 (frontend/packages/tokenscript/package.json)
- pnpm 12.8.1 (frontend/packages/mousetrap/package.json)
- pnpm 12.8.1 (frontend/packages/draft-js/package.json)
- pnpm 12.8.1 (frontend/package.json)
- pnpm 12.8.1 (exporter/package.json)
- pnpm 12.8.1 (docs/package.json)
- pnpm 12.8.1 (common/package.json)
- pnpm 12.8.1 (backend/package.json)

Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com>
2026-10-06 15:12:15 +02:00
Andrey Antukh
10504ff2bf
🐛 Sanitize SVG on binfile import (#12105)
* 🐛 Sanitize smuggled SVG on binfile import

Route imported SVG storage objects through the SVG sanitizer on
all three binfile paths (v3, v1, v2), closing the second bypass
of GHSA-ffhp-m958-qxvr. Integrity checks still run on the raw
bundle bytes first; only the persisted copy is the sanitized one.

Adds a shared sanitize-imported-svg helper with unit tests and a
v3 export-tamper-import test proving smuggled scripts no longer
survive the import.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Normalize SVG content-type on binfile import

Close the residual GHSA-ffhp-m958-qxvr bypass where a crafted
content-type spelling (uppercase, parameters) skipped the import
sanitizer. Detection now uses a shared case-insensitive predicate,
stored values are canonicalized, and the v3 re-read honors the
import size limit.

Extends the tamper tests to obfuscated spellings and adds
exhaustive v1 coverage, including the tempfile branch.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Gate binfile content-type on known storage types

Define the complete set of storage content-types in
app.common.media and enforce it on the binfile import schema, so
unknown types fail closed instead of passing through. Detection
keeps a single normalization at the boundary with an exact
predicate, and the string helpers use cuerdas.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Keep only produced types in storage-object-types

Drop apng, avif, penpot and plain text from the set: no flow
stores them, they only exist in the extension mapping table.
Every member must have a producing code path; unproduced types
stay out until some flow actually stores them.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Reject unknown content-type on v1 binfile import

Enforce the storage content-type allowlist on the v1 import
path, like v3 already does via schema. Bundles declaring types
outside the set, or none at all, fail closed with the same
media-type-not-allowed error as uploads. The sanitize branch is
explicit: SVG bytes are sanitized, anything else passes through.

AI-assisted-by: muse-spark-1.3-contributor
2026-10-06 14:21:12 +02:00
Andrey Antukh
4a4f50e7ea 🔥 Stop printing separator lines in reports
Drop the equals-sign banners from the CLJS throwable formatter,
so new frontend error reports are born without them. The JVM
branch never printed them. Adds a test asserting full reports
carry sections but no separator runs.

AI-assisted-by: Muse Spark 1.3 Free
2026-10-05 12:17:06 +02:00
Shreyash Agare
331e9168b5
🐛 Keep interactions of flex children when syncing components (#11788)
* 🐛 Keep interactions of flex children when syncing components

The flex-child sync between main and copy children propagated every
attribute in swap-keep-attrs, which includes :interactions. Since
:interactions has no sync group, the touched check never protected it,
so any change that reflowed a flex component (like toggling a child's
visibility) overwrote the copy children's interactions with the main's.

Closes #11746

AI-assisted-by: claude-opus-5
Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>

* 🐛 Add another test and simplify comments

---------

Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-05 12:04:11 +02:00
Andrey Antukh
341e5d1027 Merge remote-tracking branch 'origin/staging' into develop 2026-10-05 11:59:05 +02:00
Eva Marco
c04a3d346d
🔧 Register the missing test namespaces in the common JS runner (#12074)
Kaocha finds every test namespace on the JVM, but the JS runner only
runs the ones listed in runner.cljc. Sixteen namespaces were missing
from both lists, and files.shapes-builder-test was required but not
listed, so 125 tests never ran in JS. Add them to both lists, sorted.

The only one that failed in JS was make-tokens-status-from-tokens-lib:
(.sets base-lib) is a method call in ClojureScript. On the JVM it read
the field, but (:sets ...) on the result gave nil, so the legacy lib
was built without sets or themes. Use (.-sets base-lib) and
(.-themes base-lib), as the first part of the same test does.

AI-assisted-by: claude-opus-5-5
2026-10-05 08:25:15 +02:00
Belén Albeza
7c039231f7
🎉 Implement paste of basic HTML formatting (v3) (#12050)
* ✨ Add rich HTML paste to the v3 text editor

Pasting HTML into the v3 text editor keeps bold, italic, decoration and
text-transform, and drops fonts, colors and links. Gated behind
text-editor-wasm/v1-html-paste.

Refs #10465

AI-assisted-by: claude-opus-5-5

* ✨ Paste HTML on the canvas as a styled text shape

With v3 HTML paste on, pasting HTML on the canvas creates a text shape
with the same emphasis. Clipboard options are now read at paste time.

Refs #10465

AI-assisted-by: claude-opus-5-5
2026-10-02 15:07:12 +02:00
Elena Torró
d531c0050b
⚡ Skip builder local apply on plain translations (#12036)
Translations that touch no group-like shape or ancestor do not need
the builder objects updated, since only parent resizing reads them.

AI-assisted-by: claude-opus-5-5
2026-10-02 12:39:18 +02:00
Elena Torró
13a9da3bc3
⚡ Reduce commit cost when dropping large selections (#12032)
Collect children ids into one transient vector and track WASM shape
changes in a JS Map instead of a volatile persistent map.

AI-assisted-by: claude-opus-5-5
2026-10-02 11:22:19 +02:00
Andrey Antukh
ea3d18c0a5
✨ Audit-log docs, error-reporting flag and frontend initiator (#11997)
* ✨ Gate error reporters behind :error-reporting flag

Error reporters used to start on every boot with no way to
switch them off. They now only start when the new
:error-reporting flag is on (off by default, enable with
PENPOT_FLAGS=enable-error-reporting). The database reporter
needs the flag alone; mattermost needs the flag plus its
webhook url. Shutdown with the flag off is safe, and the
database loop now honors its runtime switch.

AI-assisted-by: muse-spark-1.3-contributor

* 📚 Add audit-log reference memory and update dev tooling

Adds the top-level audit-log reference memory with all event
flows and turns backend/audit-log into a redirect stub.
Also includes the staged dev-tooling updates (playwright
deps, mdts args, gitignore).

AI-assisted-by: muse-spark-1.3-contributor

* ✨ Assign server-side initiator to frontend audit events

Frontend audit events now carry a server-assigned initiator
derived from the x-client header (penpot-frontend goes to
app, penpot-admin-console goes to admin-console, legacy
penpot-nitrate stays on admin-console, all else goes to
app). Client-sent values are always overwritten, and the
key survives on telemetry shadow rows.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Keep initiator on frontend telemetry context

The strips-pii-keys test still asserted the old contract
(initiator stripped). The initiator only names the sending
app, so it is not personal data and must survive the
telemetry filter like on the backend channel.

AI-assisted-by: muse-spark-1.3-contributor

* 📚 Document initiator on PostHog and browser origins

Complete the audit-log memory for the latest changes: the
initiator property forwarded to PostHog on both channels
and the eventOrigin rule for admin-console browser events.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix unscaped argument in script

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-02 11:08:13 +02:00
Andrey Antukh
84c794c5b8 Merge remote-tracking branch 'origin/staging' into develop 2026-10-01 19:25:59 +02:00
Belén Albeza
60679bbbcd
🐛 Revert RTL auto-width text growing from its right edge (#12051)
Revert #11775 (commit 142f3d9de8). We are putting this fix on hold
until we settle the UX for RTL auto-width text.

Relates to #11523
2026-10-01 18:29:07 +02:00
Pablo Alba
03e24d18ce
✨ Dual-send audit log archive to Nitrate and Nexus (#12037)
Run the audit archive cron when :nexus or :admin-console is on. Ship
allowlisted events to Admin Console first (with row ids for
idempotency), then the full chunk to Nexus when :nexus is set. Mark
archived_at for the whole chunk on success, including nitrate-only
mode. Rename the gate flag from :audit-log-archive to :nexus.

AI-assisted-by: Composer

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 17:17:11 +02:00
Andrey Antukh
b5fbc4fd8c
✨ Add size limits to profile props and plugin registry (#11596)
* ✨ Add size limits to profile props and plugin registry

Bound the total serialized size of profile settings to 2 MiB
(:profile-props-max-size), checked on the merged result before
persisting, with a controlled :props-too-large error. Profiles
that already exceed the limit can still shrink but cannot grow.

Cap plugin registry entries in the shared schema (code 1 MiB, 50
plugins max, bounded name/host/description/icon) and restore rate
limiting on the plugin RPCs (profile-mutations bucket, one write
at a time per profile). The plugin manager now asks for
confirmation before removal and ignores repeated clicks while a
persist request is in flight.

Closes #11592

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Enforce plugin count cap, byte sizes and removal guard

Enforce the declared 50-plugin cap in add-profile-plugin with a
specific :too-many-plugins error (updates of existing entries
still pass); the cap lives in a shared max-plugins constant.

Measure profile props size in UTF-8 bytes instead of chars so
multibyte content cannot slip past the limit.

Cover install/remove persist logic with mocked-RPC frontend tests
(release semantics, in-flight dedupe, validation vs rollback
split) and add the missing boundary tests in common.

Expose the in-flight persist set from the plugin registry and
disable the remove button of entries being saved.

Closes #11592

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix rollback loops and restore paths in plugin registry

Restore the previous plugin version instead of dropping the entry
when a validation error rejects an update of an installed plugin.

Make compensating writes one-shot with terminal callbacks so a
persistent failure cannot ping-pong between install and remove.
Restores keep the original list position; the unused public
plugin-persisting? predicate is removed.

Pin count-before-size precedence with a dedicated test and fix
translation source refs to their canonical lines.

Closes #11592

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Guard notifications write and fix restore ordering

Route update-profile-notifications through check-props-size! so
oversized profiles cannot grow through that path; document the
exempt system writers. Remove the duplicated stale entries in
en.po, keeping the canonical translation refs.

Restore rejected plugin updates at their original list position
instead of leaving the optimistic move in place.

Closes #11592

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Skip no-op plugin removal and clarify size comments

Return early from remove-profile-plugin when the id is absent:
no wasted write, no size check, and no manufactured :plugins key
that could spuriously fail on oversized profiles.

Clarify that per-field string caps count chars while the byte
budget is enforced by profile-props-max-size.

Closes #11592

AI-assisted-by: muse-spark-1.3-contributor

* 📎 Fix formatting in rlimit.edn for profile operations

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

* 📎 Fix formatting of import-binfile/global entry

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

* ♻️ Simplify props size check and tighten plugin entry caps

Measure props with transit bytes directly instead of the
PGobject string roundtrip.

Rename check-props-size! to check-props-size: single hard limit
on the merged props, no growth comparison, and return props so
writers thread the check into the update.

Move the 2 MiB default into default-props-max-size on the
profile namespace, still overridable with the optional
:profile-props-max-size config entry.

Tighten registry-entry :code and :icon to 500 chars: they hold
manifest paths, not content.

Closes #11592

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix compatibility problems

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-01 14:31:30 +02:00
María Valderrama
9111ebb3bf
✨ Add same-subscription option to move-teams permission (#11995) 2026-10-01 13:03:55 +02:00
Andrey Antukh
34f4c8ee28 Merge remote-tracking branch 'origin/staging' into develop 2026-09-30 08:42:19 +02:00
Andrey Antukh
10ccfd2218
🐛 Gate pastes on page load and harden base-shape lookup (#11674)
* 🐛 Gate pastes on page load and harden base-shape lookup

Pasting while the workspace is still opening crashed the
session: the layer-order lookup called rseq on a missing
root children list.

Ignore paste events until the page objects are loaded (the
clipboard keeps its content, so retrying works), return empty
instead of throwing from the shared layer-order helpers, and
fall back to pasting at the pointer position when the selection
is detached from the shape tree. Selecting the page root keeps
working as before through the frame branches.

Closes #11666

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Cover props paste and root-plus-other in paste guards

Address review follow-ups on the paste-before-init fix: gate
props pasting on page readiness like the shape entries, and
route root-plus-other selections without a base shape to the
pointer fallback instead of the unguarded else branch. Pin
single-root selection to the frame path with a regression test.

Closes #11666

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Hoist page lookup out of paste-shapes gate

Bind page and page-objects once in an outer let instead of
calling lookup-page twice (once for the readiness gate and
once inside the body). No behavior change.

AI-assisted-by: muse-spark-1.3-contributor

* ♻️ Reuse bound ids in paste-shapes page lookup

Bind file-id and page-id once and pass them to the lookup-page
arity that takes both, instead of resolving the page twice and
rebinding file-id in the inner let. No behavior change.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-30 07:24:03 +02:00
Andrey Antukh
0ef35fc52a
🐛 Harden browser logging against invalid levels (#11693)
* 🐛 Stop browser logging from crashing on empty levels

Stop level->int crashes from taking down the dashboard when a
nil or unknown level reaches the browser logger. Invalid levels
now warn and are ignored in enabled?, setup! and the console
handler, which renders unknown records with a neutral fallback.

Alias the schema-legal :fatal level to :error in the browser
mappings and validate the JS-exported debug.set_logging, which
previously threw on missing arguments and wrote unreachable
keyword keys into the loggers map.

Closes #11690

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Guard logger args and strengthen logging tests

Close the residual throw paths next to the empty-level crash:
guard non-string loggers in enabled? and setup!, coerce
set_logging arguments safely, and validate logger keys.

Strengthen the regression tests so the fatal alias cannot
regress silently: enabled-logger filtering, JVM fatal and bogus
cases, setup! skip proof, and invalid-logger cases.

Related to #11690

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Address low findings from logging review

Validate the logger before the level in console-log-handler,
share a public valid-logger? predicate with debug/set-logging,
and keep warn formatting consistent across boundaries.

Document the fail-soft-FE/strict-BE split on enabled? and the
valid-level? contract on set-level!. Cover safe fallbacks, bad
logger keys, handler logger skips, and loggers-map isolation in
common tests, and add a frontend test for debug/set-logging.

Related to #11690

AI-assisted-by: muse-spark-1.3-contributor
2026-09-30 07:17:02 +02:00