* 🐛 Fix plugin postMessage channel allowing cross-plugin message injection
The global postMessage listener was broadcasting incoming messages to all
loaded plugins without validating the origin or routing to the correct
sender. This allowed any plugin (or any iframe from any origin) to inject
messages into other plugins.
- Added origin validation — messages from origins other than
window.location.origin are rejected.
- Added sender-based routing — a message is only delivered to the plugin
whose iframe contentWindow matches event.source.
- Exposed iframeWindow getters in PluginManager, PluginModalElement, and
createPlugin so the runtime can compare event.source against the correct
iframe reference.
- Updated documentation examples to include origin validation and
recommend window.location.origin over '*' for postMessage targetOrigin.
AI-assisted-by: qwen3.7-plus
* 🐛 Fix plugin origin check breaking cross-origin plugin messaging
The origin check added in the previous commit compared event.origin
against window.location.origin (Penpot own origin). Since plugins
are cross-origin by design (hosted on the plugin author domain),
this check rejected every legitimate message from every real plugin.
The event.source-based sender routing (matching iframeWindow identity)
is the correct and sufficient security mechanism - it cannot be forged
cross-origin, so the redundant origin check was removed.
- Removed event.origin check from load-plugin.ts message listener
- Updated tests to use realistic plugin origins (localhost:4202/4203)
and to verify rejection based on source identity, not origin
- Fixed documentation examples: use event.source for receiving
validation and '*' for postMessage targetOrigin
AI-assisted-by: mimo-v2.5-pro
The schema:props schema still included :plugins as a valid key,
so the closed-map validation passed instead of rejecting it.
The system-managed-props denylist only silently stripped the key
after validation, never raised an error.
Derive schema:props-writeable from schema:props using sm/dissoc-key
to exclude system-managed keys, and use it in the update-profile-props
params schema. The original schema:props remains intact for get-profile
result validation.
AI-assisted-by: mimo-v2.5
When the fetched manifest differs from the stored manifest
(code, name, etc.), show the permissions dialog instead of
silently updating — preventing execution of tampered/injected
plugins.
On fetch error, show a warning notification instead of loading
with the old manifest.
Bundled plugins (no URL) skip validation as they are trusted
with no remote source.
Completes the 3-layer defense for T3-N1-02: (1) closed
permission schema, (2) dedicated RPC methods, (3) integrity
validation on open.
AI-assisted-by: qwen3.7-plus
Add `add-profile-plugin` and `remove-profile-plugin` RPC methods for
atomic plugin registry operations, preventing manipulation via the
broader `update-profile-props` endpoint.
- Close the `:plugins` field in `update-profile-props` schema to
eliminate the mass assignment attack vector for plugin data.
- Define `valid-permissions` and a closed `schema:permissions` enum to
restrict plugin permissions to known values.
- Migrate the frontend to use the new granular RPC methods with
optimistic updates and rollback on failure.
- Add comprehensive backend tests covering valid/invalid permissions,
updates, removal, and rejection via old endpoint.
AI-assisted-by: qwen3.7-plus
* 🐛 Fix crash when a nil shape id reaches WASM modifiers
A modif-tree containing a nil shape id crashed the WASM heap write
with "Cannot read properties of null (reading '__u32_buffer')" in
propagate-modifiers. Production builds elide the upstream asserts,
so a nil id (e.g. update-dimensions called with a missing shape's
parent id in the variants flow) reached the uuid heap write.
Drop nil-keyed modif-tree entries in apply-wasm-modifiers and
set-wasm-modifiers before they reach propagate-modifiers and
set-structure-modifiers, filter nil ids at the parse level, and
guard the variant reposition/creation events against missing shapes.
AI-assisted-by: hy3
* 🐛 Fix let scoping bugs in modifiers.cljs
Two let forms in set-wasm-modifiers and apply-wasm-modifiers
had their bindings vectors closed prematurely, causing undeclared
var warnings for snap-pixel?, translation?, ids, update-shape,
options, bool-ids, undo-id, and other bindings.
In set-wasm-modifiers: removed premature ) after ] on the let
bindings so the let body wraps snap-pixel? and translation?.
In apply-wasm-modifiers: the cond form was not explicitly closed,
so all subsequent bindings (ignore-tree, options, modif-tree, ids,
update-shape, bool-ids, undo-id) were parsed as cond clauses
instead of let bindings. Added ) to close cond after :else, and
moved ] to close the bindings vector after undo-id.
AI-assisted-by: mimo-v2.5-pro
* 🐛 Fix stroke width token dropdown closing mid-interaction
The stroke-row* key included a hash of applied-tokens, forcing a full
remount whenever the async token-resolution pipeline updated that
state (e.g. after a detach action settles). If the remount landed
while the width dropdown was open, it destroyed the dropdown and
reset its open state, permanently closing it before the user could
pick a token.
Drop the hash from the key so the row only remounts on actual
structural changes (add/remove/reorder), not on every token
resolution tick.
AI-assisted-by: claude-sonnet-5
* 🐛 Fix crash when a typography token has a nil value
A composite typography token saved with no fields filled in stores a
nil :value. That value reached StyleDictionary's tokens-studio
font-styles preprocessor, which assumes a typography value is never
null and threw, crashing token resolution for every other token in
the file.
Reject the nil value at the source in the typography token form
validation, and as defense in depth, filter nil-valued tokens out of
every resolve-tokens* entry point before they reach StyleDictionary,
tagging them with the existing empty-input error instead.
Also remove a stray debug prn left in find-refs.
AI-assisted-by: claude-sonnet-5
* 🔧 Gitignore local CLAUDE.md
Keep the commit-conventions instructions file local to each
contributor's checkout instead of tracking it in the repo.
AI-assisted-by: claude-sonnet-5
* ⚡ Use a single transducer to tag invalid-value tokens
merge-invalid-value-tokens ran three separate passes over the token
map (remove, map, into) and then merged the result back in. Combine
the remove/map steps into one ns-level transducer, defined once
instead of rebuilt on every call, and pass resolved as the seed to
into so the trailing merge isn't needed either.
AI-assisted-by: claude-sonnet-5
* ♻️ Drop redundant t/testing wrapper in nil-value token test
The outer t/testing just repeated the deftest's own name and added
nothing the two inner t/testing blocks (each covering one concrete
assertion group) don't already say.
AI-assisted-by: claude-sonnet-5
* 🐛 Fail the nil-value token test on a resolution error
rx/sub! only handles the success case, so if token resolution ever
errors instead, done is never called and the async test hangs
instead of failing. Switch to rx/subs! with an error handler that
reports the failure and calls done, matching the pattern already
used elsewhere in the tokens test suite.
AI-assisted-by: claude-sonnet-5
* 🐛 Align WASM auto-width text size with HTML measurement
Include the HTML paragraph-set 1px right margin in Skia auto-width
so WASM and DOM measurements match.
* ⚡ Scope and defer post-font WASM text relayout
Track pending font faces per shape, force-relayout only affected
texts, defer stale-selrect sync until the first tiles paint, and
batch request-render after attr updates. Tolerate missing font-face
keys so callers that omit them do not blow up.
Remove the temporary serial exception introduced alongside sharding.
Sharded runs on `develop` complete in ~13 min against ~52 min for the
serial runs on `staging`, and the gap keeps widening as the suite
grows.
Also restore the JSON report as a 30-day artifact. It was dropped when
the suite moved to sharded blob reporters, which left flakiness rates
only obtainable by scraping job logs.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Post-font-load selrect fixes and position-data regeneration write
sync-attrs on texts inside mains. That made watch-component-changes
treat them as edits and run touch/sync per component, freezing large
files. Mark those commits with skip-component-sync? (same idea as
translation?) so only real user edits propagate.
The FilterInputStream proxy only implemented read() and
read(byte[], int, int). Buffered reads call read(byte[]) (and
read(byte[], int) via Clojure interop), causing ArityException
while hashing storage objects and breaking v3 imports.
Implement all read overloads and extract shared byte-count logic.
* 🐛 Fix font preview in assets breaks the font row
* 🐛 Fix font height problem also in the font dropdown
* 🐛 Fix a small bug within the changes
---------
Co-authored-by: Eva Marco <evamarcod@gmail.com>
Replaces the content-hash build key (bundle_version + docker/images tree hash) used to tag and dedupe the backend/frontend/exporter/storybook/mcp image set with sha-<commit>, matching the scheme already used by admin-console, licenses-manager and payments across the org. The check→build→promote pattern with the S3 marker is unchanged; only the key used for the marker, the immutable tag and the local bundle cache filename moves from the composite build key to the git commit sha (the bundle cache now keys on bundle_version alone, which is what it actually caches). devenv is intentionally left out of this pass, it has no versioned tagging today.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Replaces the content-hash build key (bundle_version + docker/images tree hash) used to tag and dedupe the backend/frontend/exporter/storybook/mcp image set with sha-<commit>, matching the scheme already used by admin-console, licenses-manager and payments across the org. The check→build→promote pattern with the S3 marker is unchanged; only the key used for the marker, the immutable tag and the local bundle cache filename moves from the composite build key to the git commit sha (the bundle cache now keys on bundle_version alone, which is what it actually caches). devenv is intentionally left out of this pass, it has no versioned tagging today.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* ♻️ Share text layout paragraphs across modifier clones
Store Skia paragraphs in Rc so TextContentLayout::clone keeps the
cached layout for rotate/pan modifiers. Add layout.clear() and treat
needs_update as paragraphs-empty only.
* ⚡ Reuse cached Skia paragraphs when painting text
Add try_paint_from_layout_cache to paint from TextContent.layout when
versions match, skipping ParagraphBuilder rebuild and layout on each
frame. Wire into the layered text path for plain fills without strokes
or effects.
* ⚡ Paint plain text directly onto Current
Extend can_render_directly for stroke-free text and skip the empty
save_layer in draw_text when no stroke-group opacity is set. Plain text
paints into Current without the Fills/Strokes blit.
The .custom-select-dropdown used width: fit-content which, in Firefox,
is capped to the containing block width (60 px for the font-size input).
This caused two- and three-digit font-size values (e.g. 48, 120, 1000)
to be visually clipped in the dropdown list.
Switching to width: max-content lets the dropdown expand to fit its
content regardless of the containing block width, matching the behaviour
Chrome already exhibited with fit-content.
Fixes#11008
Shapes and text with stacked solid fills were exported without fill
attributes because merge_fills builds a Skia shader that SkSVGDevice
does not serialize. Draw each fill layer separately for SVG export,
bottom to top, matching the classic exporter.
Closes#11466
Upload structural shape attrs (base, children, blur, shadows,
flex, layout-item) via multi-shape `_set_shapes_batch` FFI in
chunks of 512, then apply host attrs with use-shape selection.
The FilterInputStream proxy only implemented read() and
read(byte[], int, int). Buffered reads call read(byte[]) (and
read(byte[], int) via Clojure interop), causing ArityException
while hashing storage objects and breaking v3 imports.
Implement all read overloads and extract shared byte-count logic.
* ✨ Add text and @font-face to WASM SVG export
Emit selectable <text> elements and inject @font-face rules that
reference the font URLs registered at load time. Text fill bypasses
save_layer wrappers that SkSVGDevice drops. Text strokes are deferred
to a follow-up PR.
* 🐛 Strip trailing commas from Skia SVG text positions
Skia's SVG backend appends a trailing comma to per-glyph x/y lists.
Firefox rejects the malformed attribute and drops glyph positioning,
making exported text vanish or mis-render.
* 🐛 Fix font preview in assets breaks the font row
* 🐛 Fix font height problem also in the font dropdown
* 🐛 Fix a small bug within the changes
---------
Co-authored-by: Eva Marco <evamarcod@gmail.com>