24042 Commits

Author SHA1 Message Date
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
Dr. Dominik Jain
8efbd9b5d3
🎉 Allow MCP to work with multiple files in parallel (#11927)
* 🎉 Add multi-file session routing to the MCP server

Session routing:
- Track each user's Penpot connections by session ID.
- Dispatch explicit sessions directly; otherwise discover the sole session.
- Return a retryable error when discovery fails or is incomplete.

Plugin and tools:
- Initialize connections with a fresh session ID and file metadata.
- Share the optional sessionId schema and place it last in tool inputs.

Validation:
- 84 tests, server type checking, and MCP formatting checks pass.
- Disable test logging to avoid the logging worker shutdown hang.

AI-assisted-by: gpt-6

* 🎉 Support independent MCP connections

- Require explicit connection intent for each integrated workspace.
- Derive short session IDs from the Penpot app instance and file.
- Show and copy session IDs in the MCP menu and standalone plugin UI.
- Stop stale callbacks and reconnect attempts after disconnect.

Validate with frontend and plugin tests, type checking, live multi-tab
checks, and standalone UI checks with a simulated connection. Server
tests pass with --test-force-exit; the normal runner can hang on shutdown.

AI-assisted-by: gpt-6

* 📚 Document multi-file MCP sessions

- Explain independent connections, session selection, and copying IDs.
- Describe session ID lifetimes for integrated and standalone plugins.
- Correct browser focus, connection, and recovery guidance.

Validate with the documentation site build and diff checks.

AI-assisted-by: gpt-6

* ✨ Change behavior on session ID duplication

* ✨ Open MCP toolbar menu on hover and match menu style

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
Co-authored-by: elhombretecla <delacruzgarciajuan@gmail.com>
2026-10-02 14:22:52 +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ó
da55d6723c
⚡ Reduce per-frame CLJS work while dragging (#12027)
Memoize snap points and frame titles on the base objects and compute
the drag target frames once per gesture.
2026-10-02 12:24:04 +02:00
Elena Torró
822dbd6517
⚡ Speed up sidebar menus for large selections (#12028)
Reuse extracted colors for unchanged shapes and cache the text attrs
read from each text content.
2026-10-02 12:05:25 +02:00
Elena Torró
c148ba5267
⚡ Rebuild drag tiles per moved subtree (#12030)
WASM receives only the transforms that differ from the parent (bool
operands and masks are kept), rebuilds tiles once per moved subtree
and invalidates only the previous frame coverage.
2026-10-02 11:39:24 +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
Dr. Dominik Jain
3a0ed93360
📚 Improve MCP README (#12022)
* Make the scope of the usage instructions clear (local usage)
* Provide overview of the server's different modes of operation
* Improve npm release instructions
* Remove deprecated multi-user mode documentation
2026-10-02 11:08:53 +02:00
Elena Torró
1554847d40
⚡ Compute the drop ignore tree in a single pass (#12033)
Walk only from the top-most transformed shapes, carry the component
root down the walk and memoize each shape transform so the update
step reuses it.

AI-assisted-by: claude-opus-5-5
2026-10-02 10:51:21 +02:00
Elena Torró
09ffcb1fbc
⚡ Reduce render-wasm GPU memory and move cost on large pages (#11980) 2026-10-02 10:24:05 +02:00
Andrey Antukh
84c794c5b8 Merge remote-tracking branch 'origin/staging' into develop 2026-10-01 19:25:59 +02:00
Andrey Antukh
3f610ad793
✨ Isolate backend-test database per devenv instance (#12048)
* ✨ Isolate backend-test database per devenv instance

Each wsN now gets its own backend-test database
(penpot_test_wsN) and Valkey DB (6+N) on the shared
infra, so parallel test runs no longer wipe each
other. The main database stays shared on purpose.

manage.sh creates the test database on bring-up and
passes PENPOT_TEST_* into the main container. Test
helpers already read those vars, so no runtime code
changes. scripts/psql and db-schema gain --ws.

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

* 🐛 Fix per-instance test database creation on bring-up

psql -c does not reliably mix SQL with psql-only
commands, so the CREATE DATABASE ... WHERE NOT
EXISTS ... \gexec one-liner never ran. Use two
plain SQL round-trips instead (check pg_database,
then CREATE DATABASE) and print what happens, so a
future failure shows up in the bring-up output.

AI-assisted-by: muse-spark-1.3-contributor-free
2026-10-01 19:22:01 +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
JiMyung Lee
cc2aff6ce8
🐛 Fix typography sample overflow in non-Latin locales (#11487)
* 🐛 Fix typography sample overflow in non-Latin locales

The typography sample glyph sits in a fixed 1.5rem grid column, sized
for the English sample "Ag". Locales that translate it to something
wider wrap it onto several lines and overflow the row: ko translates
the sample as "가나다" and renders it as three stacked characters, ar
as "أسلوب خط النص" and renders it as three stacked words. This affects
the workspace assets panel (list row, detail panel and rename row) and
the dashboard library card.

Let the sample column size to its content and keep the sample on a
single line. The 1.5rem minimum is preserved, so locales whose sample
already fits — English and every locale that keeps "Ag" — render
exactly as before.

Signed-off-by: JiMyung Lee <lee.ji.myung@gmail.com>

* 🐛 Keep spacing between typography sample and name

Follow-up to review on the typography sample overflow fix. Letting
the sample grow removed the fixed-width slack that used to separate it
from the name text, so:

- Dashboard library card: `.library-name-block` no longer assumes a
  24px sample via a hard-coded calc; it flexes to the remaining space
  and the sample does not shrink.
- Typography detail panel, list row and rename/advanced-edit row: add
  an explicit `var(--sp-xs)` gap between the sample and the name, and
  let the name input shrink instead of pushing the action buttons.
- Libraries "Updates" tab: the sample div had no class and still
  wrapped per character; give it a `nowrap` class.

Signed-off-by: JiMyung Lee <lee.ji.myung@gmail.com>

---------

Signed-off-by: JiMyung Lee <lee.ji.myung@gmail.com>
Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
2026-10-01 16:02:21 +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
Andrey Antukh
b2c3fd872a
♻️ Scope organization notifications to specific WebSocket topics (#11460)
* ♻️ Scope organization notifications to specific WebSocket topics

Publish team/org notifications to team-id and organization-id topics
instead of broadcasting to all connections via uuid/zero. Dashboard and
workspace now subscribe to their current team and organization on
initialization, receiving only relevant events.

Backend: added subscribe-organization/unsubscribe-organization WebSocket
handlers and updated :close to clean up organization subscriptions.
Modified notify-team-change, notify-organization-deletion, and
notify-organization-change-sso to publish to specific topics.

Frontend: dashboard and workspace now subscribe to team-id and
organization-id (when applicable) on initialization, with nil-guard
in topic filters.

Closes #11455

AI-assisted-by: longcat-2.0

* 🔧 Remove unused session-id binding in unsubscribe-organization

Clj-kondo lint fix.

AI-assisted-by: longcat-2.0

* 🐛 Fix permission check on team ws connection

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-01 13:16:12 +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
d82f2e1005 Merge remote-tracking branch 'origin/staging' into develop 2026-10-01 12:10:40 +02:00
Andrey Antukh
8db9cb83f3 📎 Update devenv dockerfile 2026-10-01 12:09:57 +02:00
Andrey Antukh
c561a0baff Merge remote-tracking branch 'origin/staging' into develop 2026-10-01 10:59:50 +02:00
Andrey Antukh
38d6f37438 Merge remote-tracking branch 'origin/main' into staging 2026-10-01 10:59:35 +02:00
Andrey Antukh
423cb41132 📚 Update changelog 2.18.1 2026-10-01 10:58:44 +02:00
Shreyash Agare
970948159e
🐛 Fix token display for multi-selected text layers (#11944)
* 🐛 Fix token display for multi-selected text layers

When multiple text layers share the same fill token,
the design panel showed the hex value instead of the
token name. type->token-attrs derived token keys from
type->editable-attrs, which returns empty for text
shapes in the fill group. Fall back to the group's
own attrs when editable-attrs is empty.

Closes #11924

AI-assisted-by: claude-opus-4-6

* 🐛 Take text token attrs from the group attrs

The :text read mode reads values from the group attrs, so its
token attrs now come from those attrs too, instead of falling
back when the editable attrs are empty. type->token-attrs is
restored to its original form.

Add regression tests for fill tokens on multiple selections of
text shapes, and of a rect mixed with a text.

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

---------

Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-10-01 10:13:42 +02:00
Andrey Antukh
e6983a1e4e
✨ Enforce idle and absolute session expiration (#11654)
* ✨ Enforce idle and absolute session expiration

Sessions now expire on two server-side conditions: an idle window
(PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE, default 7d) and an absolute cap
from creation (PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE_ABSOLUTE, default
30d, enforced by the token :exp claim). The daily session-gc task
deletes rows that exceed either window, so idle sessions can no
longer be replayed and active sessions are not deleted at the idle
window.

Also remove the legacy v1 HTTP sessions: the http_session table and
the string-id / :ver 0 token code paths are gone. Any v1 cookie now
requires a fresh login.

Document the session expiration configuration in the technical guide
and add a backend memory describing the token, renewal and GC model.

Closes #11646

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

* 🐛 Address session-expiration review findings F1-F4

Fix the unreadable test (a stray paren broke whole-suite
discovery), enforce idle expiration on every request in
wrap-authz, fail boot fast when the absolute cap sits below
the idle window, and align config defaults with the memory
rule while fixing its migration number and stale reference.

Closes #11646

AI-assisted-by: muse-spark-1.3-contributor
2026-10-01 10:12:05 +02:00
Alonso Torres
d851f82678
🐛 Fix crashing happening on hot reloads (#12005) 2026-10-01 09:21:05 +02:00
Andrey Antukh
d67a00c1d5
✨ Normalize storage metadata with a closed schema (#11987)
* ✨ Add Malli schema for storage metadata with dual decode

Phase 1 of the storage_object.metadata migration: reads accept both
Transit and plain JSON (sniffed by the marker) and always return the
normalized shape; writes validate against a closed per-bucket Malli
schema and still serialize as Transit unless the new
:storage-metadata-as-json config flag is set.

The 0155 migration normalizes existing rows inside Transit (reference
to bucket, default bucket, drop of chunk leftovers) and is
idempotent; large instances should fake it and run the batched
script instead.

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

* ♻️ Address review findings on storage metadata Phase 1

Collapse the dead :reference leg of the gc-touched bucket fallback
(the decode always sets :bucket on non-nil metadata, so it is only
reachable with a NULL column) and fix its comment.

Pin the write flag off in the transit-assuming metadata tests so the
suite proves the same with the flag set, and add coverage for the
flag rollback contract, JSON hash survival, NULL metadata in
gc-touched, and the 0155 normalization statements.

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

* ♻️ Backfill NULLs, canonical buckets, comment fix

Backfill NULL metadata columns in 0155 via coalesce (the key-missing
rule already matches them), derive valid-buckets from the Malli schema
dispatch entries so the list lives in one place, and correct the
lookup-bucket fallback comment to NULL columns.

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

* ♻️ Defer corrupt metadata rows in storage gc-touched

Decode touched rows individually so one non-map metadata value no
longer aborts the whole chunk: corrupt rows are logged and deferred
exactly one day in the same transaction, keeping their metadata
intact for a later repair, while healthy rows process normally.

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

* ♻️ Address storage metadata phase 1 review findings

Address the review findings on the storage metadata phase 1 branch:

- Fix put-and-delete-object: it stored the object with
  ::sto/expired-at, so the row was already deleted and del-object!
  returned false. Add delete-expired-object-returns-false to keep
  the expired-delete case covered.
- Cache the Malli decoder and encoder per process. Building them
  compiles the closed multi-dispatch schema, and decode-metadata
  runs on every read path (get-object, dedup probes, GC batches).
- Catch Exception instead of Throwable in try-decode-row so JVM
  Errors are not deferred as corrupt metadata.
- Add penpot_storage_gc_poison_total, emitted from
  storage-gc-touched; wire ::mtx/metrics into its handler.
- Anchor the encoding sniff to the start of the document so a
  plain JSON value that begins with a Transit-looking prefix is
  not read as Transit.
- Cover every bucket on both encodings, a JSON roundtrip through
  the jsonb column, nil metadata, the canonical bucket set and a
  poison-only GC chunk.
- Rename private check-metadata! to check-metadata.

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

* ♻️ Simplify the storage metadata schema to a single map

Replace the per-bucket :multi dispatch with a single closed map: the
bucket is validated with ::sm/one-of over metadata-buckets (now a plain
set) and the remaining keys are typed optional fields. Per-bucket
enforcement shrinks to a one-line :fn guard requiring :file-id and :id
for file-data, whose ids the GC reads to resolve references.

- Drop the dead (sm/register! ::metadata ...): nothing references the
  schema by keyword.
- Define tempfile-bucket and upload-session-bucket in the schema and
  alias them from app.storage, removing duplicated literals.
- Keep content-type required and the map closed, so an unknown bucket
  or key still fails fast on write.

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

* ♻️ Drop input coercion from encode-metadata

encode-metadata no longer runs the json-transformer decoder before
validation. On the write path its only effect was coercing string
UUIDs to UUID, and every producer already passes native UUIDs (the
RPC profile-id, uuid/random, or binfile ids decoded as ::sm/uuid).
Reads keep decoding, so stored Transit or JSON values still come
back as native types.

- Replace encode-accepts-string-uuids with
  encode-rejects-string-uuids, pinning the stricter contract.
- Pass native UUIDs in encode-writes-plain-json-with-flag.

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

* 📚 Document each statement in the storage metadata migration

Move the per-statement rules out of the header and add a comment to each
UPDATE explaining what it does: drop chunk leftovers, promote the legacy
"~:reference" to "~:bucket", drop residual "~:reference", and backfill the
default bucket. The header keeps the scope, the encoding note, the `->`
vs `?` note and the large-instance warning.

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

* 📚 Unwrap wrapped lines in the backend storage memory

One line per bullet or paragraph, as mem:memory-maintenance requires.
Only formatting; no content change.

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

* ♻️ Defer storage GC poison rows in their own transaction

process-chunk! no longer takes poison-ids; it only processes the healthy
chunk. The deferral moves to defer-poison! and process-touched! runs it in
its own transaction, separate from the freeze/delete work. The loop still
drains while there is chunk or poison, so a batch made only of poison rows
does not leave healthy rows behind the LIMIT 10 waiting for the next run.

Add a regression test: ten poison rows plus one healthy row with a later
touched_at are all handled in the same run.

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

* ♻️ Declare per-bucket metadata requirements in one map

Replace the file-data-specific predicate with bucket-requirements, a map
from bucket to the extra keys it must carry. metadata-buckets is derived
from its keys and a single generic :fn enforces presence, so a new bucket
and its contract are one entry. organization now requires
:organization-id; file-data keeps requiring :file-id and :id.

Update the http-assets test helper to set organization-id for its
organization objects.

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

* ♻️ Drop the ! suffix from storage GC helpers

Rename the internal helpers in app.storage.gc-touched (process-chunk,
defer-poison, mark-freeze-in-bulk, ...) to drop the trailing !.

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

* ♻️ Drop the ! suffix from storage GC deleted helpers

Rename the internal helpers in app.storage.gc-deleted (clean-deleted,
delete-sobjects, delete-give-up, ...) to drop the trailing !.

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

* 🐛 Fix dedup lookup for JSON-encoded storage metadata

get-database-object-by-hash only matched the Transit keys, so once the
:storage-metadata-as-json flag wrote plain JSON rows the dedup stopped
finding them and duplicated blobs. Match both encodings with a UNION ALL
of two indexable branches.

- Add migration 0156 with the plain-key dedup index; the legacy 0068
  index stays until Transit support is removed.
- Cover it with a JSON dedup test and a Transit -> JSON cross test.

AI-assisted-by: deepseek-v4.1-flash
2026-10-01 07:19:21 +02:00
Alonso Torres
0509e2b9d2
🐛 Fix problem with connect library (#12002) 2026-09-30 17:56:01 +02:00
David Barragán Merino
86fc3dd765 👷 Pass explicit secrets to reusable workflows
Replace secrets: inherit with the secrets each reusable workflow
actually uses, and declare them under on.workflow_call.secrets in the
called workflow. Declared as required: false so behaviour is unchanged
if a secret is missing.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-30 17:01:58 +02:00
David Barragán Merino
fa3e01f7c7 👷 Disable checkout credential persistence
Set persist-credentials: false on every actions/checkout step, so the
job token is not left in .git/config for the rest of the job. No step
after checkout pushes or fetches with it. The only authenticated operation,
gh release in release.yml, uses GH_TOKEN.

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-30 17:01:58 +02:00
David Barragán Merino
d1aa07087d 👷 Pin GitHub Actions to commit SHAs
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-30 15:29:28 +02:00
Andrey Antukh
8b2ec216e4 Merge remote-tracking branch 'origin/staging' into develop 2026-09-30 14:53:40 +02:00
Andrey Antukh
7d4feac46b 🐛 Fix svg-attrs test passing MapEntry to SQL query
The import-svg-attrs-asset helper called (first result) on the
map returned by import-files!, yielding a clojure.lang.MapEntry
instead of a file UUID. PostgreSQL rejected the MapEntry as a
query parameter with "Can't infer the SQL type".

Extract the id via (first (:file-ids result)) as the neighboring
test already does.

AI-assisted-by: longcat-2.5-preview-free
2026-09-30 12:50:28 +00:00
Andrey Antukh
fc184c05ed 🔥 Remove temporal files 2026-09-30 12:47:20 +02:00
David Barragán Merino
f285b2dff7 👷 Add actionlint and zizmor checks for workflows
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-30 10:31:02 +02:00
Andrey Antukh
34f4c8ee28 Merge remote-tracking branch 'origin/staging' into develop 2026-09-30 08:42:19 +02:00
Andrey Antukh
4042daaed0 🔥 Remove serveral tmp files 2026-09-30 08:41:56 +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
Andrey Antukh
49f1936bfc Merge remote-tracking branch 'origin/staging' into develop 2026-09-29 23:07:47 +02:00
Andrey Antukh
94d6f5a25c Merge remote-tracking branch 'origin/main' into staging 2026-09-29 23:07:26 +02:00
Andrey Antukh
3b886995ba 🐛 Emit the accept-organization-invitation audit event once
The event was written twice per accepted organization invitation. The
backend submitted it, and the browser then re-submitted a copy of the
props that the backend had already put in the response under
`:organization-invitation-audit` (`handle-token :team-invitation` in
`verify-token.cljs`). Both rows carried the same name with different prop
vocabularies, and the browser copy only existed when the browser finished
the flow.

Emit the event from the backend only. It now also carries the three props
that lived in the browser copy: the organization member count before the
add, the add source, and whether the invitee also joined a team. The
origin moves to the event context as `:event-origin`. The response no
longer includes `:organization-invitation-audit`, so the browser stops
emitting the event and `verify-token.cljs` drops its
`app.main.data.event` require.

The `accept-*` events of this command now share one prop vocabulary:
`:profile-id` for the accepting profile, `:invited-by` for the inviter
and `:profile-email` for the email, replacing the mix of
`:user-id`/`:user-who-send-invitation` and `:email`.

Audit consumers of `accept-organization-invitation` now see one row per
acceptance instead of two, and must read the new prop names.

AI-assisted-by: space-bunny-free
2026-09-29 22:40:03 +02:00
David Barragán Merino
0049c8b673 🐳 Remove OpenEXR support from ImageMagick and Docker images
Penpot only accepts jpeg, png, webp, gif and svg images, so the
EXR coder was never used. It was enabled only because ImageMagick's
configure auto-detected libopenexr-dev at build time.

OpenEXR accounted for 25 CVEs (15 High) in both the exporter and
media-processor images, and was also bundled into the backend via
/opt/imagick/lib/deps, where dpkg-based scanners cannot see it.

- Build ImageMagick with --without-openexr and drop libopenexr-dev
- Drop libopenexr-3-1-30 from the imagemagick, backend, exporter,
  media-processor and devenv images
- Remove `apt-get upgrade` from the ImageMagick build stage
- Bump penpotapp/imagemagick to 7.1.2-27-1

Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-29 19:13:41 +02:00
David Barragán Merino
2d73adc926 🐳 Remove dist-upgrade from DHI-based Docker images
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
2026-09-29 19:13:41 +02:00
Marina López
90ab142f00
🐛 Add nitrate management methods (#11962) 2026-09-29 16:05:27 +02:00
Andrey Antukh
0de865748d
🐛 Keep camelCase svg attribute names when importing a penpot file (#11974)
The json reader of the binfile v3 import rewrites every key of every
entry to kebab-case, nested maps included. Shape `:svg-attrs` is the one
map whose keys are camelCase react prop names, because the svg import
path runs them through `attrs->props`, so an attribute exported as
`fillRule` came back as `:fill-rule` and was stored that way.

The renderer looks the attribute up by its camelCase name, does not find
it and falls back to the default fill rule, so a shape exported with
`fillRule: evenodd` was painted without its hole, and the attributes
panel showed `fill-rule`.

`clean-shape-post-decode` already runs on every shape right after the
schema decode, for page shapes and component objects alike, so the
repair goes there: run `:svg-attrs` back through `attrs->props`, the
same transform that built the keys. It is idempotent, so shapes that
arrive correct are left untouched.

The new tests import a real export that carries the attribute, for
page shapes and component shapes.

Closes #11954

AI-assisted-by: space-bunny-free
2026-09-29 14:15:21 +02:00
Alejandro Alonso
cba51cfb7e
🐛 Keep adjacent tile compose origins abutted (#11968)
Round the view offset once and place tiles at
origin + k * TILE_SIZE so half-pixel pans do not
open a 1px background seam between neighbors.
2026-09-29 12:38:04 +02:00
María Valderrama
be63107ed6
🐛 Fix organization/team switcher issues from UI review (#11940)
* 🐛 Fix organization/team switcher issues from UI review

* 📎 Code review
2026-09-29 12:01:45 +02:00
Andrey Antukh
dc0ea3a69c
🐛 Attribute audit events to the caller, not the response (#11952)
* 🐛 Attribute audit events to the caller, not the response

prepare-rpc-event took the event profile-id from the result map
whenever it carried one, before falling back to the caller. Any
command returning a response with a :profile-id key silently
credited the action to somebody else.

get-error-report returns the report with its decoded content
merged in, and that content holds the profile that owned the
report, so privileged reads were logged against the users whose
crashes were being inspected. verify-token on a team invitation
returns the inviter's profile-id, so accepting an invitation was
logged against the inviter.

Resolution is now ::audit/profile-id metadata, then
::rpc/profile-id, then the zero uuid; the response is never
consulted. The two verify-token branches that relied on it now
declare the profile in the result metadata. Every other command
either already declared it or returns no :profile-id; all 30
registered command namespaces were checked.

The tests that pinned the old behavior are replaced by ones
covering the new contract.

AI-assisted-by: space-bunny-free

* 🐛 Coerce the audit profile-id override to a uuid

The only sanctioned way for a command to override the profile of an
audit event is the ::audit/profile-id metadata, and the value is set
by hand in a dozen commands, some of them reading it from token
claims or other sources we do not type.

schema:event requires a uuid and submit* swallows the validation
error, so a string did not fail loudly: the row was dropped silently.
Values that cannot become a uuid are now discarded with a warning
and the event falls back to the caller, which is always a valid uuid.
A uuid, the common case, exits on the first check.

AI-assisted-by: space-bunny-free
2026-09-29 09:47:07 +02:00
Andrey Antukh
f38c7dd639
⬆️ Update deps (#11960)
* 🐛 Migrate openUIApi schema to Zod v4 function syntax

Zod 4 removed z.function().args(), which broke the
plugins-runtime build with implicit-any errors on every
openUIApi parameter and knock-on possibly-null errors on
the modal in plugin-manager.

Declare the inputs with z.function({ input: [...] }) so the
parameter and return types infer again; behavior is unchanged.

Add a regression spec covering delegation, optional args and
rejection of invalid theme and title values.

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

* 📚 Fix deprecated markdown-it-anchor permalink option in docs

Migrate docs Eleventy config to the markdown-it-anchor v10 API.
Replace the deprecated boolean permalink option with
linkInsideHeader, keeping the same symbol and class.
Bump markdown-it-anchor to v10 and related docs deps.

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

* ⬆️ Update deps

* ⬆️ Update base docker images

* 📎 Fix mcp tests
2026-09-29 09:07:34 +02:00