Persist binfile manifest metadata in file_data on import so file
statistics are available at open-workspace time. Emit a new
open-workspace-file audit event enriched with file statistics:
page count, shape count, component count, linked libraries,
design tokens, and whether the file is a shared library.
Closes#11106
AI-assisted-by: mimo-v2.5-pro
Add media type validation to upload-tempfile and upload-org-logo
management endpoints. Both stored user-supplied mtype without
checking against an allowlist. Only image types and PDF are
permitted. Non-public bucket assets now also carry
Content-Disposition: attachment to prevent inline rendering.
AI-assisted-by: mimo-v2.5-pro
Make sd-token-uuid nil-safe when accessing .original.id to prevent
crashes when StyleDictionary emits group nodes alongside real tokens.
Group nodes have an original object but no id property, causing
undefined is not an object errors during interactive token resolution
in the edit modal.
Closes#11143
AI-assisted-by: qwen3.7-plus
The sidebar measures panel numeric inputs (X, Y, width, height,
rotation) emitted one full apply-modifiers commit per DOM event with
no throttle: every arrow key-repeat, wheel tick and scrub pointermove
became update-positions / update-dimensions / increase-rotation. A
sustained gesture starved the React renderer and crashed the
workspace with error #185 (Maximum update depth exceeded).
Coalesce those bursts at the data layer (potok), following the
update-position-data debounce pattern in texts.cljs:
- update-positions is now burst-coalesced in place (its only caller
is the measures panel); new update-dimensions-coalesced and
increase-rotation-coalesced variants are used by the measures
panel, while the immediate events keep serving plugins, variants
and token application (including the delta? rotation path).
- The first event of a burst commits immediately (leading edge, so
single edits stay synchronous); further ticks commit at most once
per 50 ms (throttle); a trailing debounced flush guarantees the
exact final value lands. All payloads are absolute values, so
keeping the latest queued value per shape/attribute is lossless.
- Pending payloads are drained atomically and stale shape ids
(deleted mid-burst) are skipped. The drain stream lives until the
workspace is finalized, so bursts reuse a single subscription.
- Fewer commits per burst also means fewer undo entries; scrub drags
still produce a single entry via the input's outer transaction.
Tests: new frontend-tests.logic.sidebar-transform-coalescing-test (8
tests, legacy SVG and WASM renderer branches) guards the invariant
that a 20-event burst commits the exact final value in a handful of
commits. The previously unregistered update-position-test is wired
into the runner with WASM mock fixtures (it fails in full-suite
context without them due to a pre-existing global mock-state issue).
AI-assisted-by: kimi-k3
Add role-ceiling check to create-team-invitations and
update-team-invitation-role methods. These RPC methods allowed
team admins to grant or elevate invitations to :owner role,
bypassing the protection that exists in update-team-member-role.
The fix replicates the existing check from update-team-member-role:
reject promotion to :owner when the caller is not an owner.
Closes#11098
AI-assisted-by: qwen3.7-plus
The validate-url-allows-public-{https,http} tests relied on real DNS
resolution of example.com, which fails in containers without public
DNS access. Mock resolve-host to return a known public IP, consistent
with the pattern used by other tests in the same file.
AI-assisted-by: mimo-v2.5-pro
Apply climit with 4 global permits and 1 per-profile permit (queue 2)
to prevent connection pool exhaustion from concurrent imports. Each
import holds a DB connection for its entire duration with idle
transaction timeout disabled, so unbounded concurrency could exhaust
the pool (default 60 connections).
AI-assisted-by: mimo-v2.5-pro
The MCP workflow was named "MCP CI" while every other tests-*.yml
workflow uses the "CI: <Component>" pattern. Rename it to "CI: MCP"
for consistency in the GitHub Actions listing.
The global `proxy_set_header Host $http_host;` forwarded the client-facing
Host to internal proxy_pass calls (backend/exporter), breaking mTLS routing
in service-mesh setups (e.g. Istio STRICT mode), which match outbound
requests to a cluster based on Host/:authority.
Explicitly set `Host $proxy_host` on /api, /assets, /api/export, /readyz
and /ws/notifications so these calls always target the correct internal
service host, independent of the client's original Host header.
Fixes#10835
Signed-off-by: Sebastien MALOT <sebastien.malot@pm.gouv.fr>
Co-authored-by: Sebastien MALOT <sebastien.malot@pm.gouv.fr>
Enable Playwright's JSON reporter alongside `list` and publish a
summary of flaky tests to the job step summary. The JSON report is
kept as an artifact for 30 days so flakiness rates can be aggregated
over time.
CI already runs with `retries: 2`, so unstable tests have been passing
silently on retry. This only surfaces what the suite already absorbs;
no test behaviour changes.
The reporter in `frontend/scripts/test-e2e` becomes overridable via
`PLAYWRIGHT_REPORTER` so the local developer default stays untouched.
The comments layer lives in the viewport overlays, which are absolutely
positioned above the canvas, and the container itself carries a high
z-index. A comment bubble panned into the ruler bars therefore painted
on top of them, covering the ticks and numbers.
Clip the comments container to the area outside the ruler bars while
the rulers are visible, the same thing the `clip-handlers` clip path
already does so the selection handlers stay off the rulers. Clipping
only the comments container leaves the text editing overlay, which
shares the viewport overlays, untouched.
Fixes#11163.
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The generated /etc/nginx/overrides/server.d/mcp-locations.conf used a
plain proxy_pass target (e.g. `proxy_pass http://penpot-mcp:4402;`)
where $PENPOT_MCP_URI/$PENPOT_MCP_URI_WS are shell variables substituted
once by envsubst in nginx-entrypoint.sh at container startup, not nginx
variables. nginx resolves a literal proxy_pass hostname once when the
config loads and never re-checks it, so the existing
`resolver 127.0.0.11 valid=10s;` directive in
overrides/http.d/resolvers.conf has no effect on these three locations
- it only applies to nginx variables evaluated per-request.
In multi-container deployments where the penpot-mcp container restarts
or is recreated independently of penpot-frontend (image update, OOM,
orchestrator reschedule), it gets a new IP from Docker's/the
orchestrator's DNS, and the frontend's nginx keeps forwarding to the
old, now-dead address until penpot-frontend itself is restarted. This
surfaces to users as `wss://<host>/mcp/ws` failing to connect from the
browser after enabling the MCP plugin, with
`connect() failed (111: Connection refused)` in the frontend's nginx
logs.
Route each location through a `set $var ...; proxy_pass $var;` pair so
proxy_pass evaluates a real nginx variable, letting the pre-existing
resolver directive re-resolve penpot-mcp within its 10s TTL instead of
caching the address for the container's lifetime.
For /mcp/stream and /mcp/sse, the set value also appends
$is_args$args explicitly: when proxy_pass targets a variable AND that
variable's value includes a URI/path component, nginx does not
automatically forward the original request's query string the way it
does for a static proxy_pass target - it must be appended by hand, or
the userToken query parameter used for multi-user authentication is
silently dropped before reaching the MCP server. /mcp/ws has no path
component in its target so it isn't affected by this and needed no
such change.
Verified locally: force-recreated the penpot-mcp container onto a
different IP while leaving penpot-frontend untouched; the /mcp/ws
WebSocket upgrade kept returning 101 Switching Protocols throughout,
both immediately and after the resolver's TTL window. Separately
verified /mcp/stream: a POST with ?userToken=... now shows up
server-side as userTokenFp=<redacted first 8 chars> instead of <none>,
and an actual MCP client (Claude Code) using this proxy can now call
authenticated tools like execute_code successfully.
Signed-off-by: Jules LaPrairie <jules@lucidbox.ca>
Add `project-id` to the guard condition in `use-plugin-register`'s layout effect so the plugin "Try out" flow waits until projects have loaded.
Previously, only `plugin-url` was checked, which allowed the fetch to fire
before projects were available, sending a nil `project-id` and causing a 400
validation error from the backend.
AI-assisted-by: mimo-v2.5-pro
When a profile is deleted, only the current session was being
invalidated. Other active sessions on different devices remained
functional until the background cleanup task completed.
Add session/invalidate-all helper that deletes all sessions for
a profile by profile_id, and call it from delete-profile before
the response transform. This ensures immediate access revocation
across all devices when an account is deleted.
Closes#11114
AI-assisted-by: qwen3.7-plus
The fetch-manifest function previously had no timeout, causing the
plugin installation flow to hang indefinitely if the server accepted
the connection but never completed the response.
Added a 15-second timeout using rx/timeout to abort the request
automatically.
Closes#11119
AI-assisted-by: qwen3.7-plus
Replace the placeholder rlimit.edn with a real per-endpoint
configuration covering auth, SSRF, search, email, media and project
operations. The previous file only had a commented-out example, so
all limits fell back to the 200k/h default window.
Also propagate the evaluated `now` timestamp into both bucket and
window result maps, so consumers (e.g. soft-mode reports) can know
exactly when the limit was checked.
AI-assisted-by: minimax-m3
Share link IDs function as capability secrets — anyone possessing
the ID can read a file without authentication. The previous UUIDv8
scheme is predictable (56 bits fixed per process + 48-bit timestamp).
Changed to uuid/random (UUIDv4) for genuine unpredictability.
Closes#11116
AI-assisted-by: qwen3.7-plus
Replace standard '=' operator with MessageDigest/isEqual to prevent
timing attacks on shared key authentication middleware.
Closes#11121
AI-assisted-by: qwen3.7-plus
* 🐛 Add permission checks to WebSocket subscription handlers
Check file and team read permissions before allowing WebSocket
subscriptions to prevent resource enumeration via presence
notifications.
AI-assisted-by: mimo-v2.5-pro
* 🐛 Fix random backend test failure