The paragraph language selects the CJK typesetting rules (line breaking,
punctuation spacing, the default emphasis position and localized glyphs),
but plugins could not read or set it. `lang` holds it as a BCP 47 tag, as
the HTML `lang` attribute does; `null` or `''` removes it.
On a text shape it applies to every paragraph and reports 'mixed' when
they differ. On a text range it applies to the whole paragraphs the range
touches: `update-text-range-attrs` now writes paragraph attrs such as the
language there instead of copying them into spans. The tag pattern moves
to `schema:text-language`, shared by the content schema and the plugin
validator.
AI-assisted-by: claude-opus-5-5
Add distributed alignment to text styles, sidebar controls, CSS export,
and the Plugin API. Spread characters across horizontal and vertical
lines, including the final line, while preserving graphemes and annotations.
Keep fractional line heights consistent across painting, hit testing,
selection, and text paths to prevent baseline drift between lines.
Add tests for distribution, nonbreaking text, and fractional line geometry.
AI-assisted-by: gpt-6
Add typed one-character Zhuyin readings with upright right-side symbols
and separate neutral and non-neutral tones in both writing modes.
Persist the mode through the editor, WASM bridge and Plugin API. Preserve
annotation clearance and canvas, PNG, PDF and SVG geometry.
Validate syntax and selection boundaries, and cover schema, bridge,
rendering, UI and API behavior. Native IME and reader review remain open.
AI-assisted-by: gpt-6
Carry optional span positions through the schema, WASM header, editor,
Plugin API and CSS output. Expose Chinese controls with language defaults
and independent overrides for horizontal and vertical text.
Reserve automatic clearance per side and stack underlines, ruby and
emphasis. Preserve reading groups during decoration edits and retain
canonical position values in DOM editor style data.
Add protocol, geometry, editing and API tests, including the prior Japanese
mixed-font gutter behavior. Verify both WASM targets and live exports.
AI-assisted-by: GPT-6
Expose Japanese layout properties on Text and TextRange with validated
values and whole-shape writing mode behavior. Keep ruby readings tied
to their spans when applying styles through ranges.
Update public TypeScript declarations, the API changelog and runtime
and plugin test coverage, including active-page checks.
* 🐛 Create Plugin API shapes at the page root
Create component instances and SVG shapes at the page root so a board at
the page origin cannot capture them. Continue using the existing forced
frame option for component instances and an explicit parent frame for
SVG imports. Interactive placement retains its current behavior.
Cover component instances, SVG imports and SVG imports with images on
ordinary, flex and grid origin boards, including undo, redo, position
changes and explicit reparenting.
Closes#12086
AI-assisted-by: deepseek-v4.1-flash
Signed-off-by: choi junsoo <junsoo1172@gmail.com>
* 🐛 Preserve interactive creation placement
Cover component and SVG placement inside and outside an origin board.
Exercise the sidebar move lifecycle and verify parent preservation.
Correct the Plugin API changelog category.
AI-assisted-by: gpt-6
Signed-off-by: choi junsoo <junsoo1172@gmail.com>
* 🔧 Cover page-root creation in the Plugin API test suite
Check that LibraryComponent.instance(), createShapeFromSvg() and
createShapeFromSvgWithImages() create shapes at the page root when a
board covers the page origin. Each test removes the shape it creates,
since it lives outside the scratch board.
AI-assisted-by: claude-opus-5-5
---------
Signed-off-by: choi junsoo <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
LibraryComponent.restore() did not check that the component belongs to
the current file's library. Called on a deleted component of a connected
shared library, it restored that component and saved the change to the
library file, while remove(), annotation and duplicate() reject such
handles with a validation error.
restore() now rejects them the same way: "Can only restore components
from the current file's library".
Closes#12607
* 🐛 Fix restrict per-side stroke width tokens to rectangles in plugins API
* ♻️ Reuse per-side stroke restriction check across plugin API
Move the per-side stroke width restriction into the public
`per-side-stroke-attrs-restricted?` helper, now taking `page-id`, and reuse
it from both `shape.applyToken` and the token application path. Keep the
error message in a single `per-side-stroke-widths-message` def used by all
three rejection sites.
Wait for token resolution in the uniform stroke width regression test
instead of relying on a bare `settle`, which could assert before the
applied token lands.
AI-assisted-by: deepseek-v4.1-flash
---------
Co-authored-by: Luis de Dios <luis.dedios@kaieldos.net>
Replace the @penpot/tokenscript npm package (the TypeScript
interpreter) with penpot/tokenscript-interpreter, a Clojure and
ClojureScript interpreter that now lives in its own repository and
compiles into the frontend build.
The frontend adapter (app.main.data.tokenscript) resolves tokens with
it behind the existing :tokenscript flag and maps its results to the
values and errors the workspace uses:
- Token values are validated by the interpreter's token types, passed
by their DTCG names: `1foo` is not a dimension, a number has no
unit, an opacity is between 0 and 1. Penpot only adds its own
limits (font weight names, text decoration keywords).
- Division by zero, incompatible units, unknown functions and
function arguments out of range get messages of their own, and
token pills show the message of their error.
- Font family names are quoted before resolving, as in the adapter
this replaces (#11750): `Red Hat` stays one name.
The token form's value input passed the token name to the tokenscript
resolver in place of the typed value, and no name at all for a new
token. It now resolves the typed value under the form's token name, as
the StyleDictionary path does.
StyleDictionary stays the default until the interpreter is stable.
frontend/deps.edn pins the interpreter by tag and commit, v0.1.0.
Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
The dashboard required app.common.exceptions without ever using it,
and the plugin shape namespace still referred to
token-attr-plugin->token-attr after the per-side stroke width work
replaced its only use with expand-token-attrs. Both are leftovers of
earlier refactors; dropping them leaves clj-kondo with no warnings.
AI-assisted-by: deepseek-v4.1-flash
* 🐛 Fix font family space in tokens
* 🐛 Resolve token merge conflicts
Keep the font-family token fix and the newer
background-plugin change together in the
unreleased changelog and plugin API test suite.
AI-assisted-by: gpt-6-luna
---------
Co-authored-by: niwinz <843689+niwinz@users.noreply.github.com>
* ✨ 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>
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.
Add penpotMgmt.createProject({name, teamId?}) and
penpotMgmt.createFile({projectId, name}). Both require content:write,
trim the name and require 1 to 250 characters, and return the same
metadata as listProjects and listFiles without changing the current
view. When the new project or file belongs to the current team, the
dashboard state is updated so it shows up right away.
listProjects, listFiles and the creation methods share the summary
builders, so fileCount is now 0 instead of missing for new projects.
Update the plugin types, the MCP API docs and the MCP instructions.
Discovery:
- Add listProjects and project-scoped listFiles with content:read checks.
- Return metadata from existing RPCs in dashboard and workspace contexts.
- Reject invalid IDs and preserve backend access errors.
Documentation:
- Keep usage in API type comments and include regenerated MCP API docs.
- Remove README implementation details and consolidate the changelog.
- Add usage information to the MCP server's initial instructions
Validation:
- Pass 8 frontend tests, 81 runtime tests and the runtime build.
- Verify both APIs through live MCP calls in dashboard and workspace.
- Pass type, lint, formatting and diff checks.
AI-assisted-by: gpt-6
Signed-off-by: Dominik Jain <dominik.jain@oraios-ai.de>
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
* ✨ 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>
Drop the separate :wasm-export flag and wasm-export/v1 feature. Single
export, clipboard PNG, plugins, and batch :is-wasm now key off
render-wasm/v1 alone. The exporter trusts :is-wasm for headless WASM
and always keeps a worker pool ready.
* ✨ Auto link tokens when adding external libraries (provisional)
* 🔧 Refactor tokens-lib initialization
* 🔧 Add separated TokenStatus to store status apart of TokensLib
* 🔧 Make all status operations use the new data structure
* 🔧 Normalize status helper functions and access token sets by id
* 🔧 Rename :tokens-file to :tokens-source
* 🎉 Allow the user to choose the tokens-source of a file
* 🎉 Make tokens library readonly when it's in an external file
* 🎉 Show tokens in library summaries
* 🎉 Show source info in sidebar
* 🔧 Fix integration tests
* 🐛 Propagate changes of token values in external library
* 🎉 Layout updates
* 🔧 Refactor tokens source calculations
* 🔧 Add harder checks for nil or empty values in everything
* 🐛 Fix some integration tests
* 🔧 Add integration tests for tokens in external libs
* 🔧 Validate and repair missing tokens status
* 🎉 Make ui changes optional with config flag
* 🐛 Propagate tokens after synchronizing components in ext library
* 🐛 Propagate tokens after creating new instances
* 🐛 Propagate tokens after synchronizing tokens in ext library
* 🐛 Add a tokens source icon to libraries section (#11439)
* 🐛 Add a tokens source icon to libraries section
* 🐛 Fix ellipsis on library names
* ♻️ Remove code under flag on legacy component
* 🐛 Fix token theme name on inspect tab
* 🎉 Add changes notification (#11476)
* 🎉 Add changes notification
* ♻️ Change fn names
* 🐛 Fix tokens source label truncation and missing translations (#11533)
* 🐛 Fix tokens source label truncation and missing translations
The tokens source file name always showed, even for the current file,
and long names wrapped onto a second line instead of truncating
because the header used flex-wrap and overflow-wrap: break-word
instead of single-line ellipsis.
Show the source row unconditionally (it now displays "This file" when
the source is the current file, matching the connected-library case),
truncate the file name to one line with an ellipsis, and only attach a
tooltip with the full name when the text is actually truncated.
Replace the hardcoded UI strings with translated ones and add their
English and Spanish entries.
AI-assisted-by: claude-sonnet-5
* 🐛 Remove redundant effect dependency in tokens source
file-name-truncated? was listed as a dependency of the with-effect
that checks and observes label truncation, even though it isn't
read inside the effect body. Since the effect itself flips that
state via check-file-name-truncated, including it as a dependency
caused the ResizeObserver to be needlessly disconnected and
reconnected on every truncation change.
AI-assisted-by: claude-sonnet-5
* 🐛 Fix small visual error
* 🐛 Fix problem with plugins
* 🐛 Fix playwright tests
---------
Co-authored-by: Eva Marco <evamarcod@gmail.com>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* 🐛 Reflow auto grid cells on flow direction change
Remap only single-span auto cells to the new
:layout-grid-dir traversal order, keeping source
order, manual and area placements untouched.
Update both grid direction controls to use the
new change-grid-direction event and refresh the
stale active button on persisted direction.
Add a RED-to-GREEN model regression covering a
2x2 row-to-column transition and source-order.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Keep source order on grid flow change with areas
Skip the generic grid cell pass for the
direction event, since reflowing already
places every eligible auto item and a blind
reorder rewrites shapes around pinned areas.
Pin area/span grids with a regression test
covering direction change and source order.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Scope grid skip to direction changes only
Replace the translation flag with a narrow
skip-grid-reassignment option so component
sync and reflow metadata stay intact while
the generic grid cell pass is skipped.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Clear leftover auto cells on grid flow change
Write remapped shapes to every target auto cell and
empty leftover cells so sparse grids cannot duplicate
a child across target cells. Manual, area and
spanned cells stay untouched; source order is kept.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Pin grid flow invariants with span and manual regressions
Keep the direction-change design unchanged and lock the
claimed invariants with tests: a real 2x1 manual span
cell and an occupied manual cell stay byte-identical,
row->column->row round-trips to the original cells,
and a mixed auto/manual/span/area grid shows no shape
loss or duplication. Also drop the unused page-objects
binding from the direction-change watcher.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Unoverlap mixed grid fixture and assert movement
Move auto C to (1,3) so it no longer overlaps the 2x1
manual span at (1,2). Row auto order A,C,B,E becomes
column order A,B,E,C; assert the exact placement
while keeping pinned, source-order and no-loss
checks. Test-only change.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Unify plugin dir setter and normalize missing direction
Route GridLayoutProxy.dir through change-grid-direction so the
plugin API shares the UI direction-change path with its reflow
and source-order guarantees. Normalize a missing
:layout-grid-dir to :row at the change-grid-direction entry
point and cover it with a missing-direction regression plus a
plugin setter routing regression.
AI-assisted-by: muse-spark
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Fix grid plugin dir setter syntax
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
AI-assisted-by: opencode-go/muse-spark-1.3-contributor
* 🐛 Fix comments and tests
---------
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* 🐛 Fix fontFamilies token property mapping in Plugin API
The Plugin API exposes the font-family token property as `fontFamilies`,
while Penpot stores the canonical applied-token attribute as
`:font-family`. The bidirectional plugin/internal attribute map did not
contain that alias, so explicit `applyToken(..., ["fontFamilies"])`
validation rejected the property and applied-token readback exposed the
undocumented singular `fontFamily`.
Add `:font-family -> :font-families` to the existing canonical alias
map. The reverse mapping is derived automatically, keeping application
and readback symmetric without introducing a font-specific code path.
Closes#11405
AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>
* 🐛 Fix fontFamilies e2e test to target a text shape
The fontFamilies end-to-end regression created a flex layout frame,
whose attribute set (frame-with-layout-attributes) excludes
:font-family. The workspace token application filters such shapes,
so the internal binding and readback assertions would pass vacuously
without exercising the alias.
Target an actual `:text` shape (ctho/add-text) instead, so the test
verifies the full JS "fontFamilies" -> schema -> alias -> canonical
:font-family -> camelCase readback path.
AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>
* 🐛 Fix fontFamilies test WASM error and add changelog entry
The text-shape fontFamilies e2e applies a layout-affecting token via
wasm renderer path, hitting missing WASM exports under Node. Merge
thw/setup-wasm-mocks! into the :each fixture and add plugins
CHANGELOG entry for the fontFamilies alias fix.
AI-assisted-by: muse-spark-1.3-contributor
Related to #11566
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
---------
Signed-off-by: 최준수 <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Add content:write permission checks to Design Tokens plugin API
The Design Tokens API (tokens.cljs) had zero permission checks, allowing
any plugin to create, modify, and delete tokens, sets, and themes
regardless of granted permissions.
Add r/check-permission checks to all 22 write operations across:
- token-proxy: name, value, description, duplicate, remove, applyToken
- token-set-proxy: name, active, toggleActive, addToken, duplicate, remove
- token-theme-proxy: group, name, active, toggleActive, addSet, removeSet,
duplicate, remove
- tokens-catalog: addTheme, addSet
Follows the established pattern from comments.cljs, file.cljs, page.cljs.
Closes#11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add permission checks to shape proxy interactions, detach, export, and variants
The shape proxy (shape.cljs) had multiple operations missing permission
checks, plus a cond ordering bug that bypassed the existing content:write
check for text shapes in commit-fills!.
Fix commit-fills! cond ordering: move permission check before the
text-shape branch so text shapes are also protected.
Add content:write permission checks to:
- interaction-proxy: :trigger, :delay, :action setters, :remove method
- shape-proxy: :addInteraction, :removeInteraction, :detach
- shape-proxy: :applyToken, :switchVariant, :combineAsVariants
Add content:read permission check to:
- shape-proxy: :export (read/extraction operation)
Follows the established pattern from :resize, :rotate, :blocked setters.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add library:write permission checks to variant plugin API
The library.cljs variant operations (variant-proxy and
lib-component-proxy) had seven mutating operations that
did not check the library:write permission, allowing
any plugin to create, modify, and delete component
variants regardless of granted permissions.
Add r/check-permission checks to all 7 operations:
- variant-proxy: addVariant, addProperty,
removeProperty, renameProperty
- lib-component-proxy: transformInVariant, addVariant,
setVariantProperty
Follows the established pattern from the :name and
:path setters in the same file.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add content:write permission checks to flow and flex layout plugin API
Add permission checks to prototype flow and flex layout operations
that were missing them, allowing plugins to modify flows and layout
structure without explicit user permission.
Changes:
- page.cljs: Add content:write checks to flow-proxy (name,
startingBoard setters, remove) and page-proxy (createFlow,
removeFlow)
- flex.cljs: Add content:write checks to flex-layout-proxy
(remove, appendChild)
Follows the established pattern from tokens.cljs, shape.cljs,
and library.cljs.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add user:read permission checks to plugin API
Add permission checks to user identity accessors that were bypassing
the consent model, allowing plugins to access user data regardless
of whether the user granted user:read permission.
Changes:
- api.cljs: Add user:read checks to getCurrentUser and getActiveUsers
- comments.cljs: Add user:read checks to comment-proxy and
comment-thread-proxy owner/user getters
- file.cljs: Add user:read check to file-version-proxy createdBy getter
When user:read permission is not granted:
- getCurrentUser() returns null
- getActiveUsers() returns empty array
- owner/user/createdBy getters return null
Follows the established pattern from other permission checks in the
plugin API.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Fix problem with token API
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
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
* ⬆️ Update pnpm and its deps
* ⬆️ Update JVM dependencies in backend and common
Update several JVM dependencies across backend and common:
- passay 1.6.6 -> 2.0.0 (package reorg, ctor-based rules)
- siphash 2.0.0 -> 3.0.0 (SipHasher* renamed to SipHash*)
- lettuce-core, guava, sqlite-jdbc, jsoup, lz4-java, markdown-clj,
awssdk s3/sts, selmer, jackson-core/databind, shadow-cljs
Adapt passay validation to the new API (moved packages, constructor
configuration) and siphash to the renamed classes. Add tests for
password validation and UUID advisory-lock hashing.
AI-assisted-by: deepseek-v4-flash
* ⬆️ Update node on docker images
* 📎 Minor fixes related to pnpm12 compatibility
* ✨ Auto-link libraries during import based on slugified name
When a Penpot file is exported without bundled libraries and then
imported into a different environment, external library links are
broken because library UUIDs differ across environments.
This feature adds a heuristic to auto-relink libraries by matching
slugified library names against shared files in the target team:
- Export: embed external library metadata (id, name, slug, used-by)
in the manifest when libraries are not included in the export.
- Import: resolve external libraries by slugifying shared file names
in the destination team and matching against manifest slugs.
- Single match: auto-link silently (creates file-library-rel row).
- Multiple matches: emit SSE event so the frontend shows a selection
dialog for the user to pick the correct library.
- No match: import continues without linking (current behavior).
Backend changes:
- Extended manifest schema with optional :external-libraries field
- Added slugify-name, get-files-names, get-shared-files-for-team,
find-shared-files-by-slug helpers in app.binfile.common
- Threaded team-id into import cfg from RPC layer
- Added resolve-external-libraries and auto-link-libraries in v3
- Emit :library-candidates SSE event for multi-match cases
Frontend changes:
- Worker captures library-candidates SSE events and forwards them
- Import dialog shows auto-link notification and multi-match
selection UI with select dropdowns
- Added link-files-to-library! RPC helper for user selections
- Added en/es translations for new UI strings
Closes#9263
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Add UI for the auto-link plumbing
* ♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity
Consolidates the auto-link libraries feature into a single coherent implementation:
- Unify boolean flags (embed-assets, include-libraries, link-later)
into single ::bfc/export-type parameter
- Fix critical reference-integrity bug: pre-resolution no longer remaps
:component-file refs when no link is created (multi-match / no-permission)
- compute-link-decisions as single source of truth for auto-link logic
- 80+ backend tests covering round-trip, cross-team, permissions,
edge cases, and reference integrity
AI-assisted-by: longcat-2.0
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Eva Marco <evamarcod@gmail.com>